Skip to content

Commit

Permalink
tool: add detector for multiple new reinitializers (#2536)
Browse files Browse the repository at this point in the history
* tool: support reinitializer in slither-check-upgradeability

* MissingCalls: don't account reinitializers

* tool: add detector for multiple new reinitializers

* fix: comments and pylint
  • Loading branch information
QiuhaoLi authored Aug 23, 2024
1 parent d29f41d commit e4657f5
Show file tree
Hide file tree
Showing 18 changed files with 255 additions and 42 deletions.
31 changes: 30 additions & 1 deletion scripts/ci_test_upgradability.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

### Test slither-check-upgradeability

DIR_TESTS="tests/check-upgradeability"
DIR_TESTS="tests/tools/check_upgradeability"
solc-select install "0.5.0"
solc-select use "0.5.0"

slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_1.txt 2>&1
Expand Down Expand Up @@ -181,6 +182,32 @@ then
exit 255
fi

slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug_reinitializer --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_14.txt 2>&1
DIFF=$(diff test_14.txt "$DIR_TESTS/test_14.txt")
if [ "$DIFF" != "" ]
then
echo "slither-check-upgradeability 14 failed"
cat test_14.txt
echo ""
cat "$DIR_TESTS/test_14.txt"
echo ""
echo "$DIFF"
exit 255
fi

slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_reinitializer_V2 --new-contract-name Counter_reinitializer_V3_V4 > test_15.txt 2>&1
DIFF=$(diff test_15.txt "$DIR_TESTS/test_15.txt")
if [ "$DIFF" != "" ]
then
echo "slither-check-upgradeability 14 failed"
cat test_15.txt
echo ""
cat "$DIR_TESTS/test_15.txt"
echo ""
echo "$DIFF"
exit 255
fi

rm test_1.txt
rm test_2.txt
rm test_3.txt
Expand All @@ -194,3 +221,5 @@ rm test_10.txt
rm test_11.txt
rm test_12.txt
rm test_13.txt
rm test_14.txt
rm test_15.txt
1 change: 1 addition & 0 deletions slither/tools/upgradeability/checks/all_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
MissingCalls,
MultipleCalls,
InitializeTarget,
MultipleReinitializers,
)

from slither.tools.upgradeability.checks.functions_ids import IDCollision, FunctionShadowing
Expand Down
122 changes: 116 additions & 6 deletions slither/tools/upgradeability/checks/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CheckClassification,
)
from slither.utils.colors import red
from slither.exceptions import SlitherError

logger = logging.getLogger("Slither-check-upgradeability")

Expand All @@ -18,7 +19,7 @@ class MultipleInitTarget(Exception):
def _has_initialize_modifier(function: Function):
if not function.modifiers:
return False
return any((m.name == "initializer") for m in function.modifiers)
return any((m.name in ("initializer", "reinitializer")) for m in function.modifiers)


def _get_initialize_functions(contract):
Expand Down Expand Up @@ -164,7 +165,7 @@ class MissingInitializerModifier(AbstractCheck):

# region wiki_description
WIKI_DESCRIPTION = """
Detect if `Initializable.initializer()` is called.
Detect if `Initializable.initializer()` or `Initializable.reinitializer(uint64)` is called.
"""
# endregion wiki_description

Expand All @@ -184,7 +185,7 @@ class MissingInitializerModifier(AbstractCheck):

# region wiki_recommendation
WIKI_RECOMMENDATION = """
Use `Initializable.initializer()`.
Use `Initializable.initializer()` or `Initializable.reinitializer(uint64)`.
"""
# endregion wiki_recommendation

Expand All @@ -199,15 +200,18 @@ def _check(self):
if initializable not in self.contract.inheritance:
return []
initializer = self.contract.get_modifier_from_canonical_name("Initializable.initializer()")
reinitializer = self.contract.get_modifier_from_canonical_name(
"Initializable.reinitializer(uint64)"
)
# InitializableInitializer
if initializer is None:
if initializer is None and reinitializer is None:
return []

results = []
all_init_functions = _get_initialize_functions(self.contract)
for f in all_init_functions:
if initializer not in f.modifiers:
info = [f, " does not call the initializer modifier.\n"]
if initializer not in f.modifiers and reinitializer not in f.modifiers:
info = [f, " does not call the initializer or reinitializer modifier.\n"]
json = self.generate_result(info)
results.append(json)
return results
Expand Down Expand Up @@ -271,6 +275,9 @@ def _check(self):
all_init_functions_called = _get_all_internal_calls(most_derived_init) + [most_derived_init]
missing_calls = [f for f in all_init_functions if not f in all_init_functions_called]
for f in missing_calls:
# we don't account reinitializers
if any((m.name == "reinitializer") for m in f.modifiers):
continue
info = ["Missing call to ", f, " in ", most_derived_init, ".\n"]
json = self.generate_result(info)
results.append(json)
Expand Down Expand Up @@ -396,3 +403,106 @@ def _check(self):
]
json = self.generate_result(info)
return [json]


class MultipleReinitializers(AbstractCheck):
ARGUMENT = "multiple-new-reinitializers"
IMPACT = CheckClassification.LOW

HELP = "Multiple new reinitializers in the updated contract"
WIKI = (
"https://github.com/crytic/slither/wiki/Upgradeability-Checks#multiple-new-reinitializers"
)
WIKI_TITLE = "Multiple new reinitializers in the updated contract"

# region wiki_description
WIKI_DESCRIPTION = """
Detect multiple new reinitializers in the updated contract`.
"""
# endregion wiki_description

# region wiki_exploit_scenario
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract Counter is Initializable {
uint256 public x;
function initialize(uint256 _x) public initializer {
x = _x;
}
function changeX() public {
x++;
}
}
contract CounterV2 is Initializable {
uint256 public x;
uint256 public y;
uint256 public z;
function initializeV2(uint256 _y) public reinitializer(2) {
y = _y;
}
function initializeV3(uint256 _z) public reinitializer(3) {
z = _z;
}
function changeX() public {
x = x + y + z;
}
}
```
`CounterV2` has two reinitializers with new versions `2` and `3`. If `initializeV3()` is called first, the `initializeV2()` can't be called to initialize `y`, which may brings security risks.
"""
# endregion wiki_exploit_scenario

# region wiki_recommendation
WIKI_RECOMMENDATION = """
Do not use multiple reinitializers with higher versions in the updated contract. Please consider combining new reinitializers into a single one.
"""
# endregion wiki_recommendation

REQUIRE_CONTRACT = True
REQUIRE_CONTRACT_V2 = True

def _check(self):
contract_v1 = self.contract
contract_v2 = self.contract_v2

if contract_v2 is None:
raise SlitherError("multiple-new-reinitializers requires a V2 contract")

initializerV1 = contract_v1.get_modifier_from_canonical_name("Initializable.initializer()")
reinitializerV1 = contract_v1.get_modifier_from_canonical_name(
"Initializable.reinitializer(uint64)"
)
reinitializerV2 = contract_v2.get_modifier_from_canonical_name(
"Initializable.reinitializer(uint64)"
)

# contractV1 has initializer or reinitializer
if initializerV1 is None and reinitializerV1 is None:
return []
# contractV2 has reinitializer
if reinitializerV2 is None:
return []

initializer_funcs_v1 = _get_initialize_functions(contract_v1)
initializer_funcs_v2 = _get_initialize_functions(contract_v2)
new_reinitializer_funcs = []
for fv2 in initializer_funcs_v2:
if not any((fv2.full_name == fv1.full_name) for fv1 in initializer_funcs_v1):
new_reinitializer_funcs.append(fv2)

results = []
if len(new_reinitializer_funcs) > 1:
for f in new_reinitializer_funcs:
info = [
f,
" multiple new reinitializers which should be combined into one per upgrade.\n",
]
json = self.generate_result(info)
results.append(json)
return results
54 changes: 54 additions & 0 deletions tests/tools/check_upgradeability/contract_initialization.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ contract Initializable{
_;
}

modifier reinitializer(uint64 version){
_;
}

}

contract Contract_no_bug is Initializable{
Expand All @@ -16,6 +20,14 @@ contract Contract_no_bug is Initializable{

}

contract Contract_no_bug_reinitializer is Initializable{

function initialize() public reinitializer(2){

}

}

contract Contract_lack_to_call_modifier is Initializable{

function initialize() public {
Expand Down Expand Up @@ -47,3 +59,45 @@ contract Contract_double_call is Contract_no_bug, Contract_no_bug_inherits{
}

}

contract Contract_reinitializer_V2 is Initializable {
uint256 public x;

function initialize(uint256 _x) public initializer {
x = _x;
}

function initializeV2(uint256 _x) public reinitializer(2) {
x = _x;
}

function changeX() public {
x++;
}
}

contract Counter_reinitializer_V3_V4 is Initializable {
uint256 public x;
uint256 public y;
uint256 public z;

function initialize(uint256 _x) public initializer {
x = _x;
}

function initializeV2(uint256 _x) public reinitializer(2) {
x = _x;
}

function initializeV3(uint256 _y) public reinitializer(3) {
y = _y;
}

function initializeV4(uint256 _z) public reinitializer(4) {
z = _z;
}

function changeX() public {
x = x + y + z;
}
}
6 changes: 3 additions & 3 deletions tests/tools/check_upgradeability/test_10.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:
ContractV1.destination (tests/check-upgradeability/contractV1.sol#2) was not constant but ContractV2.destination (tests/check-upgradeability/contract_v2_constant.sol#2) is.
ContractV1.destination (tests/tools/check_upgradeability/contractV1.sol#2) was not constant but ContractV2.destination (tests/tools/check_upgradeability/contract_v2_constant.sol#2) is.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#variables-that-should-not-be-constant
INFO:Slither:
Variable missing in ContractV2 (tests/check-upgradeability/contract_v2_constant.sol#1-3): ContractV1.destination (tests/check-upgradeability/contractV1.sol#2)
Variable missing in ContractV2 (tests/tools/check_upgradeability/contract_v2_constant.sol#1-3): ContractV1.destination (tests/tools/check_upgradeability/contractV1.sol#2)
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#missing-variables
INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:4 findings, 21 detectors run
INFO:Slither:4 findings, 22 detectors run
2 changes: 1 addition & 1 deletion tests/tools/check_upgradeability/test_11.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:
ContractV1.destination (tests/check-upgradeability/contract_v1_var_init.sol#2) is a state variable with an initial value.
ContractV1.destination (tests/tools/check_upgradeability/contract_v1_var_init.sol#2) is a state variable with an initial value.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#state-variable-initialized
INFO:Slither:2 findings, 8 detectors run
2 changes: 1 addition & 1 deletion tests/tools/check_upgradeability/test_12.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initiali
INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:2 findings, 21 detectors run
INFO:Slither:2 findings, 22 detectors run
8 changes: 4 additions & 4 deletions tests/tools/check_upgradeability/test_13.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:
Different variables between ContractV1 (tests/check-upgradeability/contractV1_struct.sol#1-8) and ContractV2 (tests/check-upgradeability/contractV2_struct_bug.sol#1-8)
ContractV1.foo (tests/check-upgradeability/contractV1_struct.sol#7)
ContractV2.foo (tests/check-upgradeability/contractV2_struct_bug.sol#7)
Different variables between ContractV1 (tests/tools/check_upgradeability/contractV1_struct.sol#1-8) and ContractV2 (tests/tools/check_upgradeability/contractV2_struct_bug.sol#1-8)
ContractV1.foo (tests/tools/check_upgradeability/contractV1_struct.sol#7)
ContractV2.foo (tests/tools/check_upgradeability/contractV2_struct_bug.sol#7)
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#incorrect-variables-with-the-v2
INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:3 findings, 21 detectors run
INFO:Slither:3 findings, 22 detectors run
4 changes: 4 additions & 0 deletions tests/tools/check_upgradeability/test_14.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INFO:Slither:
Contract_no_bug_reinitializer (tests/tools/check_upgradeability/contract_initialization.sol#23-29) needs to be initialized by Contract_no_bug_reinitializer.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#25-27).
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function
INFO:Slither:1 findings, 12 detectors run
15 changes: 15 additions & 0 deletions tests/tools/check_upgradeability/test_15.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
INFO:Slither:
Contract_reinitializer_V2 (tests/tools/check_upgradeability/contract_initialization.sol#63-77) needs to be initialized by Contract_reinitializer_V2.initialize(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#66-68).
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function
INFO:Slither:
Extra variables in Counter_reinitializer_V3_V4 (tests/tools/check_upgradeability/contract_initialization.sol#79-104): Counter_reinitializer_V3_V4.y (tests/tools/check_upgradeability/contract_initialization.sol#81)
Extra variables in Counter_reinitializer_V3_V4 (tests/tools/check_upgradeability/contract_initialization.sol#79-104): Counter_reinitializer_V3_V4.z (tests/tools/check_upgradeability/contract_initialization.sol#82)
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#extra-variables-in-the-v2
INFO:Slither:
Counter_reinitializer_V3_V4.initializeV3(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#92-94) multiple new reinitializers which should be combined into one per upgrade.
Counter_reinitializer_V3_V4.initializeV4(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#96-98) multiple new reinitializers which should be combined into one per upgrade.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#multiple-new-reinitializers
INFO:Slither:
Counter_reinitializer_V3_V4 (tests/tools/check_upgradeability/contract_initialization.sol#79-104) needs to be initialized by Counter_reinitializer_V3_V4.initialize(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#84-86).
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function
INFO:Slither:6 findings, 22 detectors run
2 changes: 1 addition & 1 deletion tests/tools/check_upgradeability/test_2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initiali
INFO:Slither:
Initializable contract not found, the contract does not follow a standard initalization schema.
Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing
INFO:Slither:2 findings, 25 detectors run
INFO:Slither:2 findings, 26 detectors run
Loading

0 comments on commit e4657f5

Please sign in to comment.