Platform: Code4rena
Start Date: 18/02/2022
Pot Size: $125,000 USDC
Total HM: 13
Participants: 24
Period: 14 days
Judge: GalloDaSballo
Total Solo HM: 6
Id: 88
League: ETH
Rank: 9/24
Findings: 3
Award: $3,522.05
🌟 Selected for report: 1
🚀 Solo Findings: 0
2697.0588 USDC - $2,697.06
https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/mainnet/DepositBoxes/DepositBoxERC20.sol#L45 https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/tokens/ERC20OnChain.sol#L49-L50 https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/mainnet/DepositBoxes/DepositBoxERC20.sol#L95 https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/tokens/ERC20OnChain.sol#L60-L63
Anyone on Schain that is able to mint more tokens, other than the mint action from postMessage
in tokenManagerERC20
by bridging tokens over, can potentially drain the locked tokens in transferredAmount
in depositBoxERC20
on mainnet by calling exit with the same amount of tokens in transferredAmount[schainHash][token]
.
This will trap other users' funds on sChain and lost those funds on mainnet to the malicious attacker.
An example of proof of concept using ERC20OnChian
is given below. This case may seem to be special as the deployer of the clone contract is malicious. However, this is a potential risk that generalises to other custom contracts with any mint
functionality.
Malicious account deploys an ERC20 clone ERC20OnChain
on Schain.
By deployment, the malicious account is assigned the MINTER_ROLE on ERC20OnChain
in the constructor.
constructor( string memory contractName, string memory contractSymbol ) initializer { AccessControlEnumerableUpgradeable.__AccessControlEnumerable_init(); ERC20Upgradeable.__ERC20_init(contractName, contractSymbol); ERC20BurnableUpgradeable.__ERC20Burnable_init(); _setRoleAdmin(MINTER_ROLE, MINTER_ROLE); _setupRole(MINTER_ROLE, _msgSender()); }
tokenMangagerERC20.sol
by function addERC20TokenByOwner
.function addERC20TokenByOwner( string calldata targetChainName, address erc20OnMainChain, address erc20OnSchain )
depositBoxERC20.depositERC20
, which will increase the amount in transferredAmount[schainHash][token]
function depositERC20( string calldata schainName, address erc20OnMainnet, uint256 amount )
transferredAmount[schainHash][token]
on ERC20OnChain
function mint(address account, uint256 value) external override { require(hasRole(MINTER_ROLE, _msgSender()), "Sender is not a Minter"); _mint(account, value); }
exitToMainERC20
in TokenManagerERC20
.function exitToMainERC20( address contractOnMainnet, uint256 amount ) external override { communityLocker.checkAllowedToSendMessage(msg.sender); _exit(MAINNET_HASH, depositBox, contractOnMainnet, msg.sender, amount); }
transferredAmount[schainHash][token]
is drained to the malicious account on mainnet and victims' tokens get stranded on schain.Disable minting function to be called directly in ERC20OnChain
. Only allow minting when bridging tokens over.
#0 - cstrangedk
2022-03-04T00:37:37Z
Similar disputed issue of #17 and #39, here applies to ERC20OnChain token clone.
In production with automatic deploy
mode enabled, ERC20OnChain is deployed when token is received in DepositBox and subsequent postMessage()._sendERC20() method is executed... MINTER_ROLE and burner address is set to TokenManagerERC20, which is responsible for minting and burning token clones in response to Deposits in DepositBoxes. When automatic deploy is not abled, the SKALE chain owner is responsible for manually adding tokens and assigning the appropriate MINT and burn addresses.
Here TokenManagerERC20 mints tokens in response to a deposit https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/TokenManagers/TokenManagerERC20.sol#L231
and here burns the token in response to an exit: https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/TokenManagers/TokenManagerERC20.sol#L306
#1 - GalloDaSballo
2022-06-01T16:33:41Z
I believe the sponsor reply sheds further light into how the system will be used in 99% of cases.
That said we have to judge the codebase for how it could be exploited and used by malicious actors, and I do agree with he warden that if the MINTER_ROLE is granted to someone (let's say the admin) then they could use it to drain funds from the mainnet side of the bridge.
Because this is contingent on setup and Admin Privilege, I believe Medium Severity to be more appropriate.
Switching from a role based system to an immutable Minter Address (the bridge contract) can be used as mitigation, alternatively sChain end users will have to monitor for these types of changes and act accordingly
@openzeppelin/contracts-upgradeable 4.3.2 -> 4.5.1 @openzeppelin/hardhat-upgrades 1.10.0 -> 1.14.0
AccessControlEnumerableUpgradeable.__AccessControlEnumerable_init();
is not necessary. Just use __AccessControlEnumerable_init();
. It compiles and all tests passed.
Almost all require
message has no indication of which contract it is coming from. It is a good practice for debugging and monitoring purposes to mark the contract that reverted.
For example, https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/MessageProxy.sol#L188
the revert message could be "MessageProxy: Extra contract is not registered"
.
Many other instances are also present in almost all contracts by globally search require
.
In OpenZeppline-contracts-upgradeable
v4.5 initializer
has a new modifier https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/20380ee699406211ae88c4edd11b0af87c8abdd6/contracts/proxy/utils/Initializable.sol#L72
modifier onlyInitializing() { require(_initializing, "Initializable: contract is not initializing"); _; }
to mark and protect the initializing functions that should not be called directly called.
Instances such as
https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/mainnet/SkaleManagerClient.sol#L54-L60 , which is inherited by Twin.sol
There is inconsistency in referring to a schain, sometimes by bytes32 hash, sometimes by string name in a single contract.
For example, in SkaleManagerClient
,
https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/mainnet/SkaleManagerClient.sol#L70-L73 uses schainNash and https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/mainnet/SkaleManagerClient.sol#L42-L48 uses schainName.
Bytes32 should be preferred over string for gas-saving reasons.
The specification in the doc is inconsistent with the code. For example in https://docs.skale.network/ima/1.2.x/managing-erc20 and the diagram https://docs.skale.network/ima/1.2.x/flows#_manual_setup_steps is not consistent with the code.
Minter role in ERC20 is granted automatically either to TokenManagerERC20
or to the deployer in the constructor.
Solidity version 0.8.6
is used throughout. Many updates have been applied to the compiler. A more recent version such as 0.8.10
would work better. The latest version is 0.8.11
.
Many instances can be simplified using the OnlyRole
modifier.
For instance, https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/MessageProxyForSchain.sol#L237 and https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/tokens/ERC20OnChain.sol#L60-L61 and many other role-based require
statements.
Many functions and events do not have @param
and @return
labelled in Natspec, for instance
and many others.
initialize
functions positioned too much down the contractinitialize
function is a special feature in upgradeability patterns and often important and prone to grave consequences too. It is a good practice to put them at the beginning of the contract, like those in the OpenZeppelin-contracts-upgradeable library.
Repeated unimplemented function postIncomingMessages()
in both
and "@skalenetwork/ima-interfaces/IMessageProxy.sol" .
One can remove the one in MessageProxy.sol
without affecting any test results.
Here are two example instances
#0 - DimaStebaev
2022-03-14T14:00:52Z
L06: If SKALE chain owner adds custom ERC20 it must allow TokenManager to minting it. Custom token can have custom logic of minting
N02: In our code style we always put public
functions after external
G01: They are called inside smart contract in child contracts
#1 - GalloDaSballo
2022-05-05T15:00:45Z
Would like to see explanation of vulns like other wardens, downgrading to non-critical
Also non-critical as it's just explicit inheritance
Non-critical as if all messages are unique you don't need the contract, also the revert trace will show you the contract as well
I think this is an interesting and valid finding
Agree but Non-critical
Disagree with finding per-sponsor reply
Will leave as valid although the warden has missed some bugs from 0.8.6
Agree, but non-critical as it's just a refactoring
Agree
Per the sponsor's styleguide, consistency > dogma
I believe this is a test function but finding is valid nonetheless
I really like this report as it's short and sweet and has some interesting finds, good job!
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, CertoraInc, TerrierLover, Tomio, WatchPug, d4rk, gzeon, kenta, kyliek, m_smirnova2020, rfa, robee, saian, ye0lde
162.1871 USDC - $162.19
Division by zero will cause EVM to revert in calculating additionalGasPerMessage
in MessageProxyForMainnet.postIncomingMessages
.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Unit test shows
Error: VM Exception while processing transaction: reverted with panic code 0x12 (Division or modulo division by zero)
validate above https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/mainnet/MessageProxyForMainnet.sol#L232-L233 by
require(messages.length > 0, "MsgProxyForMainnet:zero message length");
#0 - cstrangedk
2022-03-04T20:34:42Z
IMA Agent would never send a transaction with 0 messages, as this is defined in the agent protocol. Further, theoretically if an agent could send a transaction with 0 message, transaction would be simply be reverted and agent would incur gas spend in the attempt.
#1 - GalloDaSballo
2022-04-27T15:39:49Z
I have to agree with the sponsor that the finding is gas level in nature, the worst case scenario is that the tx will revert after some computation has happened, which doesn't warrant the Med Severity
#2 - GalloDaSballo
2022-04-27T15:40:08Z
For that reason am downgrading to gas
#3 - GalloDaSballo
2022-06-01T23:47:47Z
Have given a 0 as this doesn't save gas