mirror of https://github.com/crytic/slither
commit
7b3566b4fa
@ -0,0 +1,25 @@ |
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json |
||||
language: "en" |
||||
early_access: false |
||||
knowledge_base: |
||||
learnings: |
||||
scope: auto |
||||
issues: |
||||
scope: global |
||||
reviews: |
||||
profile: "chill" |
||||
request_changes_workflow: false |
||||
high_level_summary: true |
||||
poem: false |
||||
review_status: true |
||||
collapse_walkthrough: true |
||||
auto_review: |
||||
enabled: true |
||||
ignore_title_keywords: |
||||
- "WIP" |
||||
- "DO NOT MERGE" |
||||
drafts: false |
||||
base_branches: |
||||
- dev |
||||
chat: |
||||
auto_reply: true |
@ -0,0 +1,43 @@ |
||||
--- |
||||
name: Run black (auto) |
||||
|
||||
defaults: |
||||
run: |
||||
# To load bashrc |
||||
shell: bash -ieo pipefail {0} |
||||
|
||||
on: |
||||
pull_request: |
||||
branches: [master, dev] |
||||
paths: |
||||
- "**/*.py" |
||||
|
||||
concurrency: |
||||
group: ${{ github.workflow }}-${{ github.ref }} |
||||
cancel-in-progress: true |
||||
|
||||
jobs: |
||||
build: |
||||
name: Black |
||||
runs-on: ubuntu-latest |
||||
|
||||
steps: |
||||
- name: Checkout Code |
||||
uses: actions/checkout@v4 |
||||
|
||||
- name: Set up Python 3.8 |
||||
uses: actions/setup-python@v5 |
||||
with: |
||||
python-version: 3.8 |
||||
|
||||
- name: Run black |
||||
uses: psf/black@stable |
||||
with: |
||||
options: "" |
||||
summary: false |
||||
version: "~= 22.3.0" |
||||
|
||||
- name: Annotate diff changes using reviewdog |
||||
uses: reviewdog/action-suggester@v1 |
||||
with: |
||||
tool_name: blackfmt |
@ -0,0 +1,40 @@ |
||||
name: Monthly issue metrics |
||||
on: |
||||
workflow_dispatch: |
||||
schedule: |
||||
- cron: '3 2 1 * *' |
||||
|
||||
permissions: |
||||
issues: write |
||||
pull-requests: read |
||||
|
||||
jobs: |
||||
build: |
||||
name: issue metrics |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: Get dates for last month |
||||
shell: bash |
||||
run: | |
||||
# Calculate the first day of the previous month |
||||
first_day=$(date -d "last month" +%Y-%m-01) |
||||
|
||||
# Calculate the last day of the previous month |
||||
last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) |
||||
|
||||
#Set an environment variable with the date range |
||||
echo "$first_day..$last_day" |
||||
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" |
||||
|
||||
- name: Run issue-metrics tool |
||||
uses: github/issue-metrics@v3 |
||||
env: |
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
SEARCH_QUERY: 'repo:crytic/slither is:issue created:${{ env.last_month }} -reason:"not planned" -reason:"duplicate"' |
||||
|
||||
- name: Create issue |
||||
uses: peter-evans/create-issue-from-file@v5 |
||||
with: |
||||
title: Monthly issue metrics report |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
content-filepath: ./issue_metrics.md |
@ -0,0 +1,9 @@ |
||||
- id: slither |
||||
name: Slither |
||||
description: Run Slither on your project |
||||
entry: slither |
||||
args: |
||||
- . |
||||
pass_filenames: false |
||||
language: python |
||||
files: \.sol$ |
@ -0,0 +1,12 @@ |
||||
{ |
||||
"Optimism": { |
||||
"op-mainnet": { |
||||
"ownedBy": "0xc44F30Be3eBBEfdDBB5a85168710b4f0e18f4Ff0" |
||||
} |
||||
}, |
||||
"drips": { |
||||
"ethereum": { |
||||
"ownedBy": "0xc44F30Be3eBBEfdDBB5a85168710b4f0e18f4Ff0" |
||||
} |
||||
} |
||||
} |
@ -1,84 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
### Test Detectors |
||||
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)" |
||||
|
||||
CURRENT_PATH=$(pwd) |
||||
TRAVIS_PATH='/home/travis/build/crytic/slither' |
||||
|
||||
# test_slither file.sol detectors |
||||
test_slither(){ |
||||
|
||||
expected="$DIR/../tests/expected_json/$(basename "$1" .sol).$2.json" |
||||
|
||||
# run slither detector on input file and save output as json |
||||
if ! slither "$1" --solc-disable-warnings --detect "$2" --json "$DIR/tmp-test.json"; |
||||
then |
||||
echo "Slither crashed" |
||||
exit 255 |
||||
fi |
||||
|
||||
if [ ! -f "$DIR/tmp-test.json" ]; then |
||||
echo "" |
||||
echo "Missing generated file" |
||||
echo "" |
||||
exit 1 |
||||
fi |
||||
sed "s|$CURRENT_PATH|$TRAVIS_PATH|g" "$DIR/tmp-test.json" -i |
||||
result=$(python "$DIR/json_diff.py" "$expected" "$DIR/tmp-test.json") |
||||
|
||||
rm "$DIR/tmp-test.json" |
||||
if [ "$result" != "{}" ]; then |
||||
echo "" |
||||
echo "failed test of file: $1, detector: $2" |
||||
echo "" |
||||
echo "$result" |
||||
echo "" |
||||
exit 1 |
||||
fi |
||||
|
||||
# run slither detector on input file and save output as json |
||||
if ! slither "$1" --solc-disable-warnings --detect "$2" --legacy-ast --json "$DIR/tmp-test.json"; |
||||
then |
||||
echo "Slither crashed" |
||||
exit 255 |
||||
fi |
||||
|
||||
if [ ! -f "$DIR/tmp-test.json" ]; then |
||||
echo "" |
||||
echo "Missing generated file" |
||||
echo "" |
||||
exit 1 |
||||
fi |
||||
|
||||
sed "s|$CURRENT_PATH|$TRAVIS_PATH|g" "$DIR/tmp-test.json" -i |
||||
result=$(python "$DIR/json_diff.py" "$expected" "$DIR/tmp-test.json") |
||||
|
||||
rm "$DIR/tmp-test.json" |
||||
if [ "$result" != "{}" ]; then |
||||
echo "" |
||||
echo "failed test of file: $1, detector: $2" |
||||
echo "" |
||||
echo "$result" |
||||
echo "" |
||||
exit 1 |
||||
fi |
||||
} |
||||
|
||||
# generate_expected_json file.sol detectors |
||||
generate_expected_json(){ |
||||
# generate output filename |
||||
# e.g. file: uninitialized.sol detector: uninitialized-state |
||||
# ---> uninitialized.uninitialized-state.json |
||||
output_filename="$DIR/../tests/expected_json/$(basename "$1" .sol).$2.json" |
||||
output_filename_txt="$DIR/../tests/expected_json/$(basename "$1" .sol).$2.txt" |
||||
|
||||
# run slither detector on input file and save output as json |
||||
slither "$1" --solc-disable-warnings --detect "$2" --json "$output_filename" > "$output_filename_txt" 2>&1 |
||||
|
||||
|
||||
sed "s|$CURRENT_PATH|$TRAVIS_PATH|g" "$output_filename" -i |
||||
sed "s|$CURRENT_PATH|$TRAVIS_PATH|g" "$output_filename_txt" -i |
||||
} |
||||
|
@ -1,27 +0,0 @@ |
||||
import sys |
||||
import json |
||||
from pprint import pprint |
||||
from deepdiff import DeepDiff # pip install deepdiff |
||||
|
||||
|
||||
if len(sys.argv) != 3: |
||||
print("Usage: python json_diff.py 1.json 2.json") |
||||
sys.exit(-1) |
||||
|
||||
with open(sys.argv[1], encoding="utf8") as f: |
||||
d1 = json.load(f) |
||||
|
||||
with open(sys.argv[2], encoding="utf8") as f: |
||||
d2 = json.load(f) |
||||
|
||||
|
||||
# Remove description field to allow non deterministic print |
||||
for elem in d1: |
||||
if "description" in elem: |
||||
del elem["description"] |
||||
for elem in d2: |
||||
if "description" in elem: |
||||
del elem["description"] |
||||
|
||||
|
||||
pprint(DeepDiff(d1, d2, ignore_order=True, verbose_level=2)) |
@ -0,0 +1,28 @@ |
||||
import json |
||||
from pathlib import Path |
||||
import urllib.request |
||||
|
||||
|
||||
def retrieve_json(url): |
||||
with urllib.request.urlopen(url) as response: |
||||
data = response.read().decode("utf-8") |
||||
return json.loads(data) |
||||
|
||||
|
||||
def organize_data(json_data): |
||||
version_bugs = {} |
||||
for version, info in json_data.items(): |
||||
version_bugs[version] = info["bugs"] |
||||
return version_bugs |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
bug_list_url = ( |
||||
"https://raw.githubusercontent.com/ethereum/solidity/develop/docs/bugs_by_version.json" |
||||
) |
||||
bug_data = retrieve_json(bug_list_url) |
||||
bugs_by_version = organize_data(bug_data) |
||||
|
||||
with open(Path.cwd() / Path("slither/utils/buggy_versions.py"), "w", encoding="utf-8") as file: |
||||
file.write("# pylint: disable=too-many-lines\n") |
||||
file.write(f"bugs_by_version = {bugs_by_version}") |
@ -0,0 +1,102 @@ |
||||
from typing import List |
||||
|
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class ChainlinkFeedRegistry(AbstractDetector): |
||||
|
||||
ARGUMENT = "chainlink-feed-registry" |
||||
HELP = "Detect when chainlink feed registry is used" |
||||
IMPACT = DetectorClassification.LOW |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#chainlink-feed-registry" |
||||
|
||||
WIKI_TITLE = "Chainlink Feed Registry usage" |
||||
WIKI_DESCRIPTION = "Detect when Chainlink Feed Registry is used. At the moment is only available on Ethereum Mainnet." |
||||
|
||||
# region wiki_exploit_scenario |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
import "chainlink/contracts/src/v0.8/interfaces/FeedRegistryInteface.sol" |
||||
|
||||
contract A { |
||||
FeedRegistryInterface public immutable registry; |
||||
|
||||
constructor(address _registry) { |
||||
registry = _registry; |
||||
} |
||||
|
||||
function getPrice(address base, address quote) public return(uint256) { |
||||
(, int256 price,,,) = registry.latestRoundData(base, quote); |
||||
// Do price validation |
||||
return uint256(price); |
||||
} |
||||
} |
||||
``` |
||||
If the contract is deployed on a different chain than Ethereum Mainnet the `getPrice` function will revert. |
||||
""" |
||||
# endregion wiki_exploit_scenario |
||||
|
||||
WIKI_RECOMMENDATION = "Do not use Chainlink Feed Registry outside of Ethereum Mainnet." |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
# https://github.com/smartcontractkit/chainlink/blob/8ca41fc8f722accfccccb4b1778db2df8fef5437/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol |
||||
registry_functions = [ |
||||
"decimals", |
||||
"description", |
||||
"versiom", |
||||
"latestRoundData", |
||||
"getRoundData", |
||||
"latestAnswer", |
||||
"latestTimestamp", |
||||
"latestRound", |
||||
"getAnswer", |
||||
"getTimestamp", |
||||
"getFeed", |
||||
"getPhaseFeed", |
||||
"isFeedEnabled", |
||||
"getPhase", |
||||
"getRoundFeed", |
||||
"getPhaseRange", |
||||
"getPreviousRoundId", |
||||
"getNextRoundId", |
||||
"proposeFeed", |
||||
"confirmFeed", |
||||
"getProposedFeed", |
||||
"proposedGetRoundData", |
||||
"proposedLatestRoundData", |
||||
"getCurrentPhaseId", |
||||
] |
||||
results = [] |
||||
|
||||
for contract in self.compilation_unit.contracts_derived: |
||||
nodes = [] |
||||
for target, ir in contract.all_high_level_calls: |
||||
if ( |
||||
target.name == "FeedRegistryInterface" |
||||
and ir.function_name in registry_functions |
||||
): |
||||
nodes.append(ir.node) |
||||
# Sort so output is deterministic |
||||
nodes.sort(key=lambda x: (x.node_id, x.function.full_name)) |
||||
|
||||
if len(nodes) > 0: |
||||
info: DETECTOR_INFO = [ |
||||
"The Chainlink Feed Registry is used in the ", |
||||
contract.name, |
||||
" contract. It's only available on Ethereum Mainnet, consider to not use it if the contract needs to be deployed on other chains.\n", |
||||
] |
||||
|
||||
for node in nodes: |
||||
info.extend(["\t - ", node, "\n"]) |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,78 @@ |
||||
from typing import List |
||||
|
||||
from slither.slithir.operations.internal_call import InternalCall |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class GelatoUnprotectedRandomness(AbstractDetector): |
||||
""" |
||||
Unprotected Gelato VRF requests |
||||
""" |
||||
|
||||
ARGUMENT = "gelato-unprotected-randomness" |
||||
HELP = "Call to _requestRandomness within an unprotected function" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#gelato-unprotected-randomness" |
||||
|
||||
WIKI_TITLE = "Gelato unprotected randomness" |
||||
WIKI_DESCRIPTION = "Detect calls to `_requestRandomness` within an unprotected function." |
||||
|
||||
# region wiki_exploit_scenario |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C is GelatoVRFConsumerBase { |
||||
function _fulfillRandomness( |
||||
uint256 randomness, |
||||
uint256, |
||||
bytes memory extraData |
||||
) internal override { |
||||
// Do something with the random number |
||||
} |
||||
|
||||
function bad() public { |
||||
_requestRandomness(abi.encode(msg.sender)); |
||||
} |
||||
} |
||||
``` |
||||
The function `bad` is uprotected and requests randomness.""" |
||||
# endregion wiki_exploit_scenario |
||||
|
||||
WIKI_RECOMMENDATION = ( |
||||
"Function that request randomness should be allowed only to authorized users." |
||||
) |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results = [] |
||||
|
||||
for contract in self.compilation_unit.contracts_derived: |
||||
if "GelatoVRFConsumerBase" in [c.name for c in contract.inheritance]: |
||||
for function in contract.functions_entry_points: |
||||
if not function.is_protected() and ( |
||||
nodes_request := [ |
||||
ir.node |
||||
for ir in function.all_internal_calls() |
||||
if isinstance(ir, InternalCall) |
||||
and ir.function_name == "_requestRandomness" |
||||
] |
||||
): |
||||
# Sort so output is deterministic |
||||
nodes_request.sort(key=lambda x: (x.node_id, x.function.full_name)) |
||||
|
||||
for node in nodes_request: |
||||
info: DETECTOR_INFO = [ |
||||
function, |
||||
" is unprotected and request randomness from Gelato VRF\n\t- ", |
||||
node, |
||||
"\n", |
||||
] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,92 @@ |
||||
from typing import List |
||||
|
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.core.cfg.node import Node |
||||
from slither.core.variables.variable import Variable |
||||
from slither.core.expressions import TypeConversion, Literal |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class OptimismDeprecation(AbstractDetector): |
||||
|
||||
ARGUMENT = "optimism-deprecation" |
||||
HELP = "Detect when deprecated Optimism predeploy or function is used." |
||||
IMPACT = DetectorClassification.LOW |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#optimism-deprecation" |
||||
|
||||
WIKI_TITLE = "Optimism deprecated predeploy or function" |
||||
WIKI_DESCRIPTION = "Detect when deprecated Optimism predeploy or function is used." |
||||
|
||||
# region wiki_exploit_scenario |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
interface GasPriceOracle { |
||||
function scalar() external view returns (uint256); |
||||
} |
||||
|
||||
contract Test { |
||||
GasPriceOracle constant OPT_GAS = GasPriceOracle(0x420000000000000000000000000000000000000F); |
||||
|
||||
function a() public { |
||||
OPT_GAS.scalar(); |
||||
} |
||||
} |
||||
``` |
||||
The call to the `scalar` function of the Optimism GasPriceOracle predeploy always revert. |
||||
""" |
||||
# endregion wiki_exploit_scenario |
||||
|
||||
WIKI_RECOMMENDATION = "Do not use the deprecated components." |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results = [] |
||||
|
||||
deprecated_predeploys = [ |
||||
"0x4200000000000000000000000000000000000000", # LegacyMessagePasser |
||||
"0x4200000000000000000000000000000000000001", # L1MessageSender |
||||
"0x4200000000000000000000000000000000000002", # DeployerWhitelist |
||||
"0x4200000000000000000000000000000000000013", # L1BlockNumber |
||||
] |
||||
|
||||
for contract in self.compilation_unit.contracts_derived: |
||||
use_deprecated: List[Node] = [] |
||||
|
||||
for _, ir in contract.all_high_level_calls: |
||||
# To avoid FPs we assume predeploy contracts are always assigned to a constant and typecasted to an interface |
||||
# and we check the target address of a high level call. |
||||
if ( |
||||
isinstance(ir.destination, Variable) |
||||
and isinstance(ir.destination.expression, TypeConversion) |
||||
and isinstance(ir.destination.expression.expression, Literal) |
||||
): |
||||
if ir.destination.expression.expression.value in deprecated_predeploys: |
||||
use_deprecated.append(ir.node) |
||||
|
||||
if ( |
||||
ir.destination.expression.expression.value |
||||
== "0x420000000000000000000000000000000000000F" |
||||
and ir.function_name in ("overhead", "scalar", "getL1GasUsed") |
||||
): |
||||
use_deprecated.append(ir.node) |
||||
# Sort so output is deterministic |
||||
use_deprecated.sort(key=lambda x: (x.node_id, x.function.full_name)) |
||||
if len(use_deprecated) > 0: |
||||
info: DETECTOR_INFO = [ |
||||
"A deprecated Optimism predeploy or function is used in the ", |
||||
contract.name, |
||||
" contract.\n", |
||||
] |
||||
|
||||
for node in use_deprecated: |
||||
info.extend(["\t - ", node, "\n"]) |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,155 @@ |
||||
from typing import List |
||||
|
||||
from slither.core.cfg.node import Node |
||||
from slither.core.declarations import Function, FunctionContract |
||||
from slither.slithir.operations import HighLevelCall |
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class OutOfOrderRetryable(AbstractDetector): |
||||
|
||||
ARGUMENT = "out-of-order-retryable" |
||||
HELP = "Out-of-order retryable transactions" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#out-of-order-retryable-transactions" |
||||
|
||||
WIKI_TITLE = "Out-of-order retryable transactions" |
||||
WIKI_DESCRIPTION = "Out-of-order retryable transactions" |
||||
|
||||
# region wiki_exploit_scenario |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract L1 { |
||||
function doStuffOnL2() external { |
||||
// Retryable A |
||||
IInbox(inbox).createRetryableTicket({ |
||||
to: l2contract, |
||||
l2CallValue: 0, |
||||
maxSubmissionCost: maxSubmissionCost, |
||||
excessFeeRefundAddress: msg.sender, |
||||
callValueRefundAddress: msg.sender, |
||||
gasLimit: gasLimit, |
||||
maxFeePerGas: maxFeePerGas, |
||||
data: abi.encodeCall(l2contract.claim_rewards, ()) |
||||
}); |
||||
// Retryable B |
||||
IInbox(inbox).createRetryableTicket({ |
||||
to: l2contract, |
||||
l2CallValue: 0, |
||||
maxSubmissionCost: maxSubmissionCost, |
||||
excessFeeRefundAddress: msg.sender, |
||||
callValueRefundAddress: msg.sender, |
||||
gasLimit: gas, |
||||
maxFeePerGas: maxFeePerGas, |
||||
data: abi.encodeCall(l2contract.unstake, ()) |
||||
}); |
||||
} |
||||
} |
||||
|
||||
contract L2 { |
||||
function claim_rewards() public { |
||||
// rewards is computed based on balance and staking period |
||||
uint unclaimed_rewards = _compute_and_update_rewards(); |
||||
token.safeTransfer(msg.sender, unclaimed_rewards); |
||||
} |
||||
|
||||
// Call claim_rewards before unstaking, otherwise you lose your rewards |
||||
function unstake() public { |
||||
_free_rewards(); // clean up rewards related variables |
||||
balance = balance[msg.sender]; |
||||
balance[msg.sender] = 0; |
||||
staked_token.safeTransfer(msg.sender, balance); |
||||
} |
||||
} |
||||
``` |
||||
Bob calls `doStuffOnL2` but the first retryable ticket calling `claim_rewards` fails. The second retryable ticket calling `unstake` is executed successfully. As a result, Bob loses his rewards.""" |
||||
# endregion wiki_exploit_scenario |
||||
|
||||
WIKI_RECOMMENDATION = "Do not rely on the order or successful execution of retryable tickets." |
||||
|
||||
key = "OUTOFORDERRETRYABLE" |
||||
|
||||
# pylint: disable=too-many-branches |
||||
def _detect_multiple_tickets( |
||||
self, function: FunctionContract, node: Node, visited: List[Node] |
||||
) -> None: |
||||
if node in visited: |
||||
return |
||||
|
||||
visited = visited + [node] |
||||
|
||||
fathers_context = [] |
||||
|
||||
for father in node.fathers: |
||||
if self.key in father.context: |
||||
fathers_context += father.context[self.key] |
||||
|
||||
# Exclude path that dont bring further information |
||||
if node in self.visited_all_paths: |
||||
if all(f_c in self.visited_all_paths[node] for f_c in fathers_context): |
||||
return |
||||
else: |
||||
self.visited_all_paths[node] = [] |
||||
|
||||
self.visited_all_paths[node] = self.visited_all_paths[node] + fathers_context |
||||
|
||||
if self.key not in node.context: |
||||
node.context[self.key] = fathers_context |
||||
|
||||
# include ops from internal function calls |
||||
internal_ops = [] |
||||
for ir in node.internal_calls: |
||||
if isinstance(ir.function, Function): |
||||
internal_ops += ir.function.all_slithir_operations() |
||||
|
||||
# analyze node for retryable tickets |
||||
for ir in node.irs + internal_ops: |
||||
if ( |
||||
isinstance(ir, HighLevelCall) |
||||
and isinstance(ir.function, Function) |
||||
and ir.function.name |
||||
in [ |
||||
"createRetryableTicket", |
||||
"outboundTransferCustomRefund", |
||||
"unsafeCreateRetryableTicket", |
||||
] |
||||
): |
||||
node.context[self.key].append(node) |
||||
|
||||
if len(node.context[self.key]) > 1: |
||||
self.results.append(node.context[self.key]) |
||||
return |
||||
|
||||
for son in node.sons: |
||||
self._detect_multiple_tickets(function, son, visited) |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results = [] |
||||
|
||||
# pylint: disable=attribute-defined-outside-init |
||||
self.results = [] |
||||
self.visited_all_paths = {} |
||||
|
||||
for contract in self.compilation_unit.contracts: |
||||
for function in contract.functions: |
||||
if ( |
||||
function.is_implemented |
||||
and function.contract_declarer == contract |
||||
and function.entry_point |
||||
): |
||||
function.entry_point.context[self.key] = [] |
||||
self._detect_multiple_tickets(function, function.entry_point, []) |
||||
|
||||
for multiple_tickets in self.results: |
||||
info = ["Multiple retryable tickets created in the same function:\n"] |
||||
|
||||
for x in multiple_tickets: |
||||
info += ["\t -", x, "\n"] |
||||
|
||||
json = self.generate_result(info) |
||||
results.append(json) |
||||
|
||||
return results |
@ -0,0 +1,73 @@ |
||||
from typing import List |
||||
|
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class PythDeprecatedFunctions(AbstractDetector): |
||||
""" |
||||
Documentation: This detector finds deprecated Pyth function calls |
||||
""" |
||||
|
||||
ARGUMENT = "pyth-deprecated-functions" |
||||
HELP = "Detect Pyth deprecated functions" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-deprecated-functions" |
||||
WIKI_TITLE = "Pyth deprecated functions" |
||||
WIKI_DESCRIPTION = "Detect when a Pyth deprecated function is used" |
||||
WIKI_RECOMMENDATION = ( |
||||
"Do not use deprecated Pyth functions. Visit https://api-reference.pyth.network/." |
||||
) |
||||
|
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; |
||||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; |
||||
|
||||
contract C { |
||||
|
||||
IPyth pyth; |
||||
|
||||
constructor(IPyth _pyth) { |
||||
pyth = _pyth; |
||||
} |
||||
|
||||
function A(bytes32 priceId) public { |
||||
PythStructs.Price memory price = pyth.getPrice(priceId); |
||||
... |
||||
} |
||||
} |
||||
``` |
||||
The function `A` uses the deprecated `getPrice` Pyth function. |
||||
""" |
||||
|
||||
def _detect(self): |
||||
DEPRECATED_PYTH_FUNCTIONS = [ |
||||
"getValidTimePeriod", |
||||
"getEmaPrice", |
||||
"getPrice", |
||||
] |
||||
results: List[Output] = [] |
||||
|
||||
for contract in self.compilation_unit.contracts_derived: |
||||
for target_contract, ir in contract.all_high_level_calls: |
||||
if ( |
||||
target_contract.name == "IPyth" |
||||
and ir.function_name in DEPRECATED_PYTH_FUNCTIONS |
||||
): |
||||
info: DETECTOR_INFO = [ |
||||
"The following Pyth deprecated function is used\n\t- ", |
||||
ir.node, |
||||
"\n", |
||||
] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,147 @@ |
||||
from typing import List |
||||
|
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.utils.output import Output |
||||
from slither.slithir.operations import Binary, Assignment, Unpack, SolidityCall |
||||
from slither.core.variables import Variable |
||||
from slither.core.declarations.solidity_variables import SolidityFunction |
||||
from slither.core.cfg.node import Node |
||||
|
||||
|
||||
class ChronicleUncheckedPrice(AbstractDetector): |
||||
""" |
||||
Documentation: This detector finds calls to Chronicle oracle where the returned price is not checked |
||||
https://docs.chroniclelabs.org/Resources/FAQ/Oracles#how-do-i-check-if-an-oracle-becomes-inactive-gets-deprecated |
||||
""" |
||||
|
||||
ARGUMENT = "chronicle-unchecked-price" |
||||
HELP = "Detect when Chronicle price is not checked." |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#chronicle-unchecked-price" |
||||
|
||||
WIKI_TITLE = "Chronicle unchecked price" |
||||
WIKI_DESCRIPTION = "Chronicle oracle is used and the price returned is not checked to be valid. For more information https://docs.chroniclelabs.org/Resources/FAQ/Oracles#how-do-i-check-if-an-oracle-becomes-inactive-gets-deprecated." |
||||
|
||||
# region wiki_exploit_scenario |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C { |
||||
IChronicle chronicle; |
||||
|
||||
constructor(address a) { |
||||
chronicle = IChronicle(a); |
||||
} |
||||
|
||||
function bad() public { |
||||
uint256 price = chronicle.read(); |
||||
} |
||||
``` |
||||
The `bad` function gets the price from Chronicle by calling the read function however it does not check if the price is valid.""" |
||||
# endregion wiki_exploit_scenario |
||||
|
||||
WIKI_RECOMMENDATION = "Validate that the price returned by the oracle is valid." |
||||
|
||||
def _var_is_checked(self, nodes: List[Node], var_to_check: Variable) -> bool: |
||||
visited = set() |
||||
checked = False |
||||
|
||||
while nodes: |
||||
if checked: |
||||
break |
||||
next_node = nodes[0] |
||||
nodes = nodes[1:] |
||||
|
||||
for node_ir in next_node.all_slithir_operations(): |
||||
if isinstance(node_ir, Binary) and var_to_check in node_ir.read: |
||||
checked = True |
||||
break |
||||
# This case is for tryRead and tryReadWithAge |
||||
# if the isValid boolean is checked inside a require(isValid) |
||||
if ( |
||||
isinstance(node_ir, SolidityCall) |
||||
and node_ir.function |
||||
in ( |
||||
SolidityFunction("require(bool)"), |
||||
SolidityFunction("require(bool,string)"), |
||||
SolidityFunction("require(bool,error)"), |
||||
) |
||||
and var_to_check in node_ir.read |
||||
): |
||||
checked = True |
||||
break |
||||
|
||||
if next_node not in visited: |
||||
visited.add(next_node) |
||||
for son in next_node.sons: |
||||
if son not in visited: |
||||
nodes.append(son) |
||||
return checked |
||||
|
||||
# pylint: disable=too-many-nested-blocks,too-many-branches |
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
|
||||
for contract in self.compilation_unit.contracts_derived: |
||||
for target_contract, ir in sorted( |
||||
contract.all_high_level_calls, |
||||
key=lambda x: (x[1].node.node_id, x[1].node.function.full_name), |
||||
): |
||||
if target_contract.name in ("IScribe", "IChronicle") and ir.function_name in ( |
||||
"read", |
||||
"tryRead", |
||||
"readWithAge", |
||||
"tryReadWithAge", |
||||
"latestAnswer", |
||||
"latestRoundData", |
||||
): |
||||
found = False |
||||
if ir.function_name in ("read", "latestAnswer"): |
||||
# We need to iterate the IRs as we are not always sure that the following IR is the assignment |
||||
# for example in case of type conversion it isn't |
||||
for node_ir in ir.node.irs: |
||||
if isinstance(node_ir, Assignment): |
||||
possible_unchecked_variable_ir = node_ir.lvalue |
||||
found = True |
||||
break |
||||
elif ir.function_name in ("readWithAge", "tryRead", "tryReadWithAge"): |
||||
# We are interested in the first item of the tuple |
||||
# readWithAge : value |
||||
# tryRead/tryReadWithAge : isValid |
||||
for node_ir in ir.node.irs: |
||||
if isinstance(node_ir, Unpack) and node_ir.index == 0: |
||||
possible_unchecked_variable_ir = node_ir.lvalue |
||||
found = True |
||||
break |
||||
elif ir.function_name == "latestRoundData": |
||||
found = False |
||||
for node_ir in ir.node.irs: |
||||
if isinstance(node_ir, Unpack) and node_ir.index == 1: |
||||
possible_unchecked_variable_ir = node_ir.lvalue |
||||
found = True |
||||
break |
||||
|
||||
# If we did not find the variable assignment we know it's not checked |
||||
checked = ( |
||||
self._var_is_checked(ir.node.sons, possible_unchecked_variable_ir) |
||||
if found |
||||
else False |
||||
) |
||||
|
||||
if not checked: |
||||
info: DETECTOR_INFO = [ |
||||
"Chronicle price is not checked to be valid in ", |
||||
ir.node.function, |
||||
"\n\t- ", |
||||
ir.node, |
||||
"\n", |
||||
] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,79 @@ |
||||
from typing import List |
||||
|
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.utils.output import Output |
||||
from slither.slithir.operations import Member, Binary, Assignment |
||||
|
||||
|
||||
class PythUnchecked(AbstractDetector): |
||||
""" |
||||
Documentation: This detector finds deprecated Pyth function calls |
||||
""" |
||||
|
||||
# To be overriden in the derived class |
||||
PYTH_FUNCTIONS = [] |
||||
PYTH_FIELD = "" |
||||
|
||||
# pylint: disable=too-many-nested-blocks |
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
|
||||
for contract in self.compilation_unit.contracts_derived: |
||||
for target_contract, ir in contract.all_high_level_calls: |
||||
if target_contract.name == "IPyth" and ir.function_name in self.PYTH_FUNCTIONS: |
||||
# We know for sure the second IR in the node is an Assignment operation of the TMP variable. Example: |
||||
# Expression: price = pyth.getEmaPriceNoOlderThan(id,age) |
||||
# IRs: |
||||
# TMP_0(PythStructs.Price) = HIGH_LEVEL_CALL, dest:pyth(IPyth), function:getEmaPriceNoOlderThan, arguments:['id', 'age'] |
||||
# price(PythStructs.Price) := TMP_0(PythStructs.Price) |
||||
assert isinstance(ir.node.irs[1], Assignment) |
||||
return_variable = ir.node.irs[1].lvalue |
||||
checked = False |
||||
|
||||
possible_unchecked_variable_ir = None |
||||
nodes = ir.node.sons |
||||
visited = set() |
||||
while nodes: |
||||
if checked: |
||||
break |
||||
next_node = nodes[0] |
||||
nodes = nodes[1:] |
||||
|
||||
for node_ir in next_node.all_slithir_operations(): |
||||
# We are accessing the unchecked_var field of the returned Price struct |
||||
if ( |
||||
isinstance(node_ir, Member) |
||||
and node_ir.variable_left == return_variable |
||||
and node_ir.variable_right.name == self.PYTH_FIELD |
||||
): |
||||
possible_unchecked_variable_ir = node_ir.lvalue |
||||
# We assume that if unchecked_var happens to be inside a binary operation is checked |
||||
if ( |
||||
isinstance(node_ir, Binary) |
||||
and possible_unchecked_variable_ir is not None |
||||
and possible_unchecked_variable_ir in node_ir.read |
||||
): |
||||
checked = True |
||||
break |
||||
|
||||
if next_node not in visited: |
||||
visited.add(next_node) |
||||
for son in next_node.sons: |
||||
if son not in visited: |
||||
nodes.append(son) |
||||
|
||||
if not checked: |
||||
info: DETECTOR_INFO = [ |
||||
f"Pyth price {self.PYTH_FIELD} field is not checked in ", |
||||
ir.node.function, |
||||
"\n\t- ", |
||||
ir.node, |
||||
"\n", |
||||
] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,50 @@ |
||||
from slither.detectors.abstract_detector import DetectorClassification |
||||
from slither.detectors.statements.pyth_unchecked import PythUnchecked |
||||
|
||||
|
||||
class PythUncheckedConfidence(PythUnchecked): |
||||
""" |
||||
Documentation: This detector finds when the confidence level of a Pyth price is not checked |
||||
""" |
||||
|
||||
ARGUMENT = "pyth-unchecked-confidence" |
||||
HELP = "Detect when the confidence level of a Pyth price is not checked" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-unchecked-confidence" |
||||
WIKI_TITLE = "Pyth unchecked confidence level" |
||||
WIKI_DESCRIPTION = "Detect when the confidence level of a Pyth price is not checked" |
||||
WIKI_RECOMMENDATION = "Check the confidence level of a Pyth price. Visit https://docs.pyth.network/price-feeds/best-practices#confidence-intervals for more information." |
||||
|
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; |
||||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; |
||||
|
||||
contract C { |
||||
IPyth pyth; |
||||
|
||||
constructor(IPyth _pyth) { |
||||
pyth = _pyth; |
||||
} |
||||
|
||||
function bad(bytes32 id, uint256 age) public { |
||||
PythStructs.Price memory price = pyth.getEmaPriceNoOlderThan(id, age); |
||||
// Use price |
||||
} |
||||
} |
||||
``` |
||||
The function `A` uses the price without checking its confidence level. |
||||
""" |
||||
|
||||
PYTH_FUNCTIONS = [ |
||||
"getEmaPrice", |
||||
"getEmaPriceNoOlderThan", |
||||
"getEmaPriceUnsafe", |
||||
"getPrice", |
||||
"getPriceNoOlderThan", |
||||
"getPriceUnsafe", |
||||
] |
||||
|
||||
PYTH_FIELD = "conf" |
@ -0,0 +1,52 @@ |
||||
from slither.detectors.abstract_detector import DetectorClassification |
||||
from slither.detectors.statements.pyth_unchecked import PythUnchecked |
||||
|
||||
|
||||
class PythUncheckedPublishTime(PythUnchecked): |
||||
""" |
||||
Documentation: This detector finds when the publishTime of a Pyth price is not checked |
||||
""" |
||||
|
||||
ARGUMENT = "pyth-unchecked-publishtime" |
||||
HELP = "Detect when the publishTime of a Pyth price is not checked" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = ( |
||||
"https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-unchecked-publishtime" |
||||
) |
||||
WIKI_TITLE = "Pyth unchecked publishTime" |
||||
WIKI_DESCRIPTION = "Detect when the publishTime of a Pyth price is not checked" |
||||
WIKI_RECOMMENDATION = "Check the publishTime of a Pyth price." |
||||
|
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; |
||||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; |
||||
|
||||
contract C { |
||||
IPyth pyth; |
||||
|
||||
constructor(IPyth _pyth) { |
||||
pyth = _pyth; |
||||
} |
||||
|
||||
function bad(bytes32 id) public { |
||||
PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); |
||||
// Use price |
||||
} |
||||
} |
||||
``` |
||||
The function `A` uses the price without checking its `publishTime` coming from the `getEmaPriceUnsafe` function. |
||||
""" |
||||
|
||||
PYTH_FUNCTIONS = [ |
||||
"getEmaPrice", |
||||
# "getEmaPriceNoOlderThan", |
||||
"getEmaPriceUnsafe", |
||||
"getPrice", |
||||
# "getPriceNoOlderThan", |
||||
"getPriceUnsafe", |
||||
] |
||||
|
||||
PYTH_FIELD = "publishTime" |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue