mirror of https://github.com/crytic/slither
Merge pull request #2582 from crytic/dev-gelato-vrf-detector
Add Gelato VRF unprotected request detectorpull/2588/head
commit
d10f7ebf09
@ -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,6 @@ |
||||
C.bad() (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#42-44) is unprotected and request randomness from Gelato VRF |
||||
- id = _requestRandomness(abi.encode(msg.sender)) (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#43) |
||||
|
||||
C.good2() (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#51-54) is unprotected and request randomness from Gelato VRF |
||||
- id = _requestRandomness(abi.encode(msg.sender)) (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#53) |
||||
|
@ -0,0 +1,62 @@ |
||||
// Mock GelatoVRFConsumerBase for what we need |
||||
abstract contract GelatoVRFConsumerBase { |
||||
bool[] public requestPending; |
||||
mapping(uint256 => bytes32) public requestedHash; |
||||
|
||||
function _fulfillRandomness( |
||||
uint256 randomness, |
||||
uint256 requestId, |
||||
bytes memory extraData |
||||
) internal virtual; |
||||
|
||||
function _requestRandomness( |
||||
bytes memory extraData |
||||
) internal returns (uint256 requestId) { |
||||
requestId = uint256(requestPending.length); |
||||
requestPending.push(); |
||||
requestPending[requestId] = true; |
||||
|
||||
bytes memory data = abi.encode(requestId, extraData); |
||||
uint256 round = 111; |
||||
|
||||
bytes memory dataWithRound = abi.encode(round, data); |
||||
bytes32 requestHash = keccak256(dataWithRound); |
||||
|
||||
requestedHash[requestId] = requestHash; |
||||
} |
||||
|
||||
} |
||||
|
||||
contract C is GelatoVRFConsumerBase { |
||||
address owner; |
||||
mapping(address => bool) authorized; |
||||
|
||||
function _fulfillRandomness( |
||||
uint256 randomness, |
||||
uint256, |
||||
bytes memory extraData |
||||
) internal override { |
||||
// Do something with the random number |
||||
} |
||||
|
||||
function bad() public { |
||||
uint id = _requestRandomness(abi.encode(msg.sender)); |
||||
} |
||||
|
||||
function good() public { |
||||
require(msg.sender == owner); |
||||
uint id = _requestRandomness(abi.encode(msg.sender)); |
||||
} |
||||
|
||||
// This is currently a FP due to the limitation of function.is_protected |
||||
function good2() public { |
||||
require(authorized[msg.sender]); |
||||
uint id = _requestRandomness(abi.encode(msg.sender)); |
||||
} |
||||
|
||||
function good3() public { |
||||
if (msg.sender != owner) { revert(); } |
||||
uint id = _requestRandomness(abi.encode(msg.sender)); |
||||
} |
||||
|
||||
} |
Binary file not shown.
Loading…
Reference in new issue