mirror of https://github.com/crytic/slither
Merge pull request #2340 from crytic/feat/out-of-order-retryable-detector
Feat/out of order retryable detectorpull/2341/head
commit
1854c147d5
@ -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 internal_call in node.internal_calls: |
||||||
|
if isinstance(internal_call, Function): |
||||||
|
internal_ops += internal_call.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,16 @@ |
|||||||
|
Multiple retryable tickets created in the same function: |
||||||
|
-Y(msg.sender).createRetryableTicket(address(1),0,0,address(0),address(0),0,0,) (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#62-70) |
||||||
|
-Y(msg.sender).createRetryableTicket(address(2),0,0,address(0),address(0),0,0,) (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#72-80) |
||||||
|
|
||||||
|
Multiple retryable tickets created in the same function: |
||||||
|
-good2() (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#95) |
||||||
|
-good2() (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#96) |
||||||
|
|
||||||
|
Multiple retryable tickets created in the same function: |
||||||
|
-Y(msg.sender).createRetryableTicket(address(1),0,0,address(0),address(0),0,0,) (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#40-48) |
||||||
|
-Y(msg.sender).createRetryableTicket(address(2),0,0,address(0),address(0),0,0,) (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#50-58) |
||||||
|
|
||||||
|
Multiple retryable tickets created in the same function: |
||||||
|
-Y(msg.sender).createRetryableTicket(address(1),0,0,address(0),address(0),0,0,) (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#83-91) |
||||||
|
-good2() (tests/e2e/detectors/test_data/out-of-order-retryable/0.8.20/out_of_order_retryable.sol#92) |
||||||
|
|
@ -0,0 +1,109 @@ |
|||||||
|
interface Y { |
||||||
|
function createRetryableTicket( |
||||||
|
address to, |
||||||
|
uint256 l2CallValue, |
||||||
|
uint256 maxSubmissionCost, |
||||||
|
address excessFeeRefundAddress, |
||||||
|
address callValueRefundAddress, |
||||||
|
uint256 gasLimit, |
||||||
|
uint256 maxFeePerGas, |
||||||
|
bytes calldata data |
||||||
|
) external payable returns (uint256); |
||||||
|
} |
||||||
|
|
||||||
|
contract X { |
||||||
|
function good() external { |
||||||
|
if (true) { |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(1), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
} else { |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(2), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
} |
||||||
|
} |
||||||
|
function bad1() external { |
||||||
|
if (true) { |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(1), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
} |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(2), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
|
||||||
|
} |
||||||
|
function bad2() external { |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(1), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
|
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(2), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
} |
||||||
|
function bad3() external { |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(1), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
good2(); |
||||||
|
} |
||||||
|
function bad4() external { |
||||||
|
good2(); |
||||||
|
good2(); |
||||||
|
} |
||||||
|
function good2() internal { |
||||||
|
Y(msg.sender).createRetryableTicket( |
||||||
|
address(2), |
||||||
|
0, |
||||||
|
0, |
||||||
|
address(0), |
||||||
|
address(0), |
||||||
|
0, |
||||||
|
0, |
||||||
|
""); |
||||||
|
} |
||||||
|
} |
Binary file not shown.
Loading…
Reference in new issue