Skip to content

ci: add Slither static-analysis #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion .github/workflows/contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,59 @@ jobs:
run: npx hardhat compile

- name: Run hardhat tests
run: npx hardhat test
run: npx hardhat test

slither:
if: github.event.pull_request.draft == false
needs: foundry
runs-on: ubuntu-latest
permissions:
statuses: write
security-events: write

steps:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false

- uses: actions/setup-node@v4
with:
node-version: '18'

- run: yarn install --frozen-lockfile

- uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0
with:
version: nightly

- name: Build contracts
run: forge build --build-info --out out --evm-version cancun

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- run: |
python -m pip install --upgrade pip
pip install slither-analyzer==0.11.3

- name: Run Slither (High severity gate)
run: |
slither . \
--filter-paths "src/test/*|src/mocks/*|scripts/*|node_modules" \
--foundry-out-directory out \
--exclude-dependencies \
--exclude-medium \
--exclude-low \
--exclude-informational \
--fail-high \
--json slither-report.json \
--markdown-root slither-report.md

- uses: actions/upload-artifact@v4
with:
name: slither-static-analysis
path: |
slither-report.json
slither-report.md
6 changes: 6 additions & 0 deletions src/L1/L1ScrollMessenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger {
require(_from != xDomainMessageSender, "Invalid message sender");

xDomainMessageSender = _from;
// xDomainMessageSender serves as reentrancy guard (notInExecution modifier).
// slither-disable-next-line reentrancy-eth
(bool success, ) = _to.call{value: _value}(_message);
// reset value to refund gas.
xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER;
Expand Down Expand Up @@ -316,6 +318,8 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger {
// @note If the list is very long, the message may never be dropped.
while (true) {
// If the `_lastIndex` is from `messageQueueV2`, it will revert in `messageQueueV1.dropCrossDomainMessage`.
// call to messageQueueV1 is safe.
// slither-disable-next-line reentrancy-no-eth
IL1MessageQueueV1(messageQueueV1).dropCrossDomainMessage(_lastIndex);
_lastIndex = prevReplayIndex[_lastIndex];
if (_lastIndex == 0) break;
Expand All @@ -328,6 +332,8 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger {

// set execution context
xDomainMessageSender = ScrollConstants.DROP_XDOMAIN_MESSAGE_SENDER;
// xDomainMessageSender serves as reentrancy guard (notInExecution modifier).
// slither-disable-next-line reentrancy-eth
IMessageDropCallback(_from).onDropMessage{value: _value}(_message);
// clear execution context
xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER;
Expand Down
4 changes: 4 additions & 0 deletions src/L1/gateways/L1ETHGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ contract L1ETHGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCallback

// @note can possible trigger reentrant call to messenger,
// but it seems not a big problem.
// no reentrancy risk (nonReentrant modifier).
// slither-disable-next-line arbitrary-send-eth
(bool _success, ) = _to.call{value: _amount}("");
require(_success, "ETH transfer failed");

Expand All @@ -108,6 +110,8 @@ contract L1ETHGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCallback

require(_amount == msg.value, "msg.value mismatch");

// no reentrancy risk (nonReentrant modifier).
// slither-disable-next-line arbitrary-send-eth
(bool _success, ) = _receiver.call{value: _amount}("");
require(_success, "ETH transfer failed");

Expand Down
8 changes: 7 additions & 1 deletion src/L1/gateways/L1GatewayRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,19 @@ contract L1GatewayRouter is OwnableUpgradeable, IL1GatewayRouter {
*****************************/

/// @inheritdoc IL1GatewayRouter
/// @dev All the gateways should have reentrancy guard to prevent potential attack though this function.
/// @dev All the gateways should have reentrancy guard to prevent potential attack through this function.
function requestERC20(
address _sender,
address _token,
uint256 _amount
) external onlyInContext returns (uint256) {
address _caller = _msgSender();
uint256 _balance = IERC20Upgradeable(_token).balanceOf(_caller);

// only whitelisted caller allowed (onlyInContext modifier).
// slither-disable-next-line arbitrary-send-erc20
IERC20Upgradeable(_token).safeTransferFrom(_sender, _caller, _amount);

_amount = IERC20Upgradeable(_token).balanceOf(_caller) - _balance;
return _amount;
}
Expand Down Expand Up @@ -157,6 +161,8 @@ contract L1GatewayRouter is OwnableUpgradeable, IL1GatewayRouter {
// encode msg.sender with _data
bytes memory _routerData = abi.encode(_msgSender(), _data);

// gatewayInContext serves as reentrancy guard (onlyNotInContext modifier).
// slither-disable-next-line reentrancy-eth
IL1ERC20Gateway(_gateway).depositERC20AndCall{value: msg.value}(_token, _to, _amount, _routerData, _gasLimit);

// leave deposit context
Expand Down
4 changes: 4 additions & 0 deletions src/L1/gateways/L1WETHGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ contract L1WETHGateway is L1ERC20Gateway {
require(_l2Token == l2WETH, "l2 token not WETH");
require(_amount == msg.value, "msg.value mismatch");

// deposit to WETH token is safe.
// slither-disable-next-line arbitrary-send-eth
IWETH(_l1Token).deposit{value: _amount}();
}

Expand All @@ -114,6 +116,8 @@ contract L1WETHGateway is L1ERC20Gateway {
require(_token == WETH, "token not WETH");
require(_amount == msg.value, "msg.value mismatch");

// deposit to WETH token is safe.
// slither-disable-next-line arbitrary-send-eth
IWETH(_token).deposit{value: _amount}();
}

Expand Down
2 changes: 2 additions & 0 deletions src/L2/L2ScrollMessenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ contract L2ScrollMessenger is ScrollMessengerBase, IL2ScrollMessenger {

xDomainMessageSender = _from;
// solhint-disable-next-line avoid-low-level-calls
// no reentrancy risk, only alias(l1ScrollMessenger) can call relayMessage.
// slither-disable-next-line reentrancy-eth
(bool success, ) = _to.call{value: _value}(_message);
// reset value to refund gas.
xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER;
Expand Down
2 changes: 2 additions & 0 deletions src/L2/gateways/L2ETHGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ contract L2ETHGateway is ScrollGatewayBase, IL2ETHGateway {
require(msg.value == _amount, "msg.value mismatch");

// solhint-disable-next-line avoid-low-level-calls
// no reentrancy risk.
// slither-disable-next-line arbitrary-send-eth
(bool _success, ) = _to.call{value: _amount}("");
require(_success, "ETH transfer failed");

Expand Down
2 changes: 2 additions & 0 deletions src/L2/gateways/L2WETHGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ contract L2WETHGateway is L2ERC20Gateway {
require(_l2Token == WETH, "l2 token not WETH");
require(_amount == msg.value, "msg.value mismatch");

// deposit to WETH token is safe.
// slither-disable-next-line arbitrary-send-eth
IWETH(_l2Token).deposit{value: _amount}();
IERC20Upgradeable(_l2Token).safeTransfer(_to, _amount);

Expand Down
4 changes: 3 additions & 1 deletion src/L2/predeploys/L2TxFeeVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ contract L2TxFeeVault is OwnableBase {

emit Withdrawal(_value, recipient, msg.sender);

// no fee provided
// @note no fee provided
// transfer to messenger is safe.
// slither-disable-next-line arbitrary-send-eth
IL2ScrollMessenger(messenger).sendMessage{value: _value}(
recipient,
_value,
Expand Down
2 changes: 2 additions & 0 deletions src/L2/predeploys/WrappedEther.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ contract WrappedEther is ERC20Permit {

_burn(_sender, wad);

// no reentrancy risk (checks-effects-interactions).
// slither-disable-next-line arbitrary-send-eth
(bool success, ) = _sender.call{value: wad}("");
require(success, "withdraw ETH failed");

Expand Down
15 changes: 14 additions & 1 deletion src/batch-bridge/L1BatchBridgeGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,13 @@ contract L1BatchBridgeGateway is AccessControlEnumerableUpgradeable, ReentrancyG
function depositERC20(address token, uint96 amount) external nonReentrant {
if (token == address(0)) revert ErrorIncorrectMethodForETHDeposit();

// common practice to handle fee on transfer token.
uint256 beforeBalance = IERC20Upgradeable(token).balanceOf(address(this));

// no reentrancy risk (nonReentrant modifier).
// slither-disable-next-line reentrancy-no-eth
IERC20Upgradeable(token).safeTransferFrom(_msgSender(), address(this), amount);

// common practice to handle fee on transfer token.
amount = uint96(IERC20Upgradeable(token).balanceOf(address(this)) - beforeBalance);

_deposit(token, _msgSender(), amount);
Expand Down Expand Up @@ -280,13 +284,17 @@ contract L1BatchBridgeGateway is AccessControlEnumerableUpgradeable, ReentrancyG
accumulatedFee = IERC20Upgradeable(token).balanceOf(address(this)) - cachedTokenState.pending;
}
if (accumulatedFee > 0) {
// no reentrancy risk (onlyRole modifier).
// slither-disable-next-line reentrancy-eth, reentrancy-no-eth
_transferToken(token, feeVault, accumulatedFee);
}

// deposit token to L2
BatchState memory cachedBatchState = batches[token][cachedTokenState.pendingBatchIndex];
address l2Token;
if (token == address(0)) {
// transfer to messenger is safe.
// slither-disable-next-line arbitrary-send-eth
IL1ScrollMessenger(messenger).sendMessage{value: cachedBatchState.amount + depositFee}(
counterpart,
cachedBatchState.amount,
Expand All @@ -298,6 +306,9 @@ contract L1BatchBridgeGateway is AccessControlEnumerableUpgradeable, ReentrancyG
l2Token = IL1ERC20Gateway(gateway).getL2ERC20Address(token);
IERC20Upgradeable(token).safeApprove(gateway, 0);
IERC20Upgradeable(token).safeApprove(gateway, cachedBatchState.amount);

// transfer to whitelisted gateway is safe.
// slither-disable-next-line arbitrary-send-eth
IL1ERC20Gateway(gateway).depositERC20{value: depositFee}(
token,
counterpart,
Expand Down Expand Up @@ -329,6 +340,8 @@ contract L1BatchBridgeGateway is AccessControlEnumerableUpgradeable, ReentrancyG
// refund keeper fee
unchecked {
if (msg.value > depositFee + batchBridgeFee) {
// no reentrancy risk (onlyRole modifier).
// slither-disable-next-line reentrancy-eth, reentrancy-no-eth
_transferToken(address(0), _msgSender(), msg.value - depositFee - batchBridgeFee);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/batch-bridge/L2BatchBridgeGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ contract L2BatchBridgeGateway is AccessControlEnumerableUpgradeable {
) private returns (bool success) {
if (token == address(0)) {
// We add gas limit here to avoid DDOS from malicious receiver.
// slither-disable-next-line arbitrary-send-eth
(success, ) = receiver.call{value: amount, gas: SAFE_ETH_TRANSFER_GAS_LIMIT}("");
} else {
// We perform a low level call here, to bypass Solidity's return data size checking mechanism.
Expand Down
2 changes: 2 additions & 0 deletions src/misc/ScrollOwner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ contract ScrollOwner is AccessControlEnumerable {
bytes calldata _data
) private {
// solhint-disable-next-line avoid-low-level-calls
// no reentrancy risk.
// slither-disable-next-line arbitrary-send-eth
(bool success, ) = _target.call{value: _value}(_data);
if (!success) {
// solhint-disable-next-line no-inline-assembly
Expand Down
Loading