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: 1/24
Findings: 6
Award: $31,523.27
๐ Selected for report: 2
๐ Solo Findings: 2
๐ Selected for report: cmichel
19978.213 USDC - $19,978.21
The postIncomingMessages
function calls _callReceiverContract(fromChainHash, messages[i], startingCounter + 1)
which gives control to a contract that is potentially attacker controlled before updating the incomingMessageCounter
.
for (uint256 i = 0; i < messages.length; i++) { // @audit re-entrant, can submit same postIncomingMessages again _callReceiverContract(fromChainHash, messages[i], startingCounter + 1); } connectedChains[fromChainHash].incomingMessageCounter += messages.length;
The attacker can re-enter into the postIncomingMessages
function and submit the same messages again, creating a replay attack.
Note that the startingCounter
is the way how messages are prevented from replay attacks here, there are no further nonces.
Attacker can submit two cross-chain messages to be executed:
transfer
.Some node submits the postIncomingMessages(params)
transaction, transfers 1000 USDC, then calls the attackers contract, who can themself call postIncomingMessages(params)
again, receive 1000 USDC a second time, and stop the recursion.
Add a messageInProgressLocker
modifier to postIncomingMessages
as was done in MessageProxyForMainnet
.
#0 - GalloDaSballo
2022-05-08T15:16:56Z
@cstrangedk It seems like the https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/bls/SkaleVerifier.sol#L45
is a view function (no state change), as such how would the verify function be able to prove that a signature wasn't reused in the same tx, and in the same block?
I believe that as long as no state change has happened (verify
-> _callReceiverContract
-> reEntrancy -> verify
), you still would be able to us the same signature as messages.length (and no other storage variable) was changed.
What do you think?
#1 - GalloDaSballo
2022-06-01T16:06:25Z
After a conversation with the sponsor we agreed that the codebase in the contest is slightly different from the one they have internally.
As such the reEntrancy is exploitable in this version of the code. Because one signature allows to replay the message multiple times, leading to draining of funds potentially, I agree that High Severity is appropriate
#2 - GalloDaSballo
2022-06-01T16:06:52Z
The Sponsor has mitigated as of the start of the contest, however the code with the vulnerability made it's way into the repo in scope
#3 - cstrangedk
2022-11-11T16:31:11Z
Resolved via https://github.com/skalenetwork/IMA/pull/1054
2697.0588 USDC - $2,697.06
A connected chain can be removed which will make all pending messages fail.
If the chain is reinitialized again at some point, its incomingMessageCounter
will start at zero and allow replay attacks of all previous messages.
function removeConnectedChain(string memory schainName) public virtual override onlyChainConnector { bytes32 schainHash = keccak256(abi.encodePacked(schainName)); require(connectedChains[schainHash].inited, "Chain is not initialized"); delete connectedChains[schainHash]; } function _addConnectedChain(bytes32 schainHash) internal onlyChainConnector { require(!connectedChains[schainHash].inited,"Chain is already connected"); connectedChains[schainHash] = ConnectedChainInfo({ // @audit counters reset to zero incomingMessageCounter: 0, outgoingMessageCounter: 0, inited: true }); }
Not sure if a chain should ever be removed. If it's removed once, it should be permanently disabled.
#0 - DimaStebaev
2022-03-10T15:25:19Z
This one duplicates #57.
#1 - GalloDaSballo
2022-06-01T16:04:49Z
Dup of #57
1618.2353 USDC - $1,618.24
Some mainnet tokens (like USDT) don't correctly implement the EIP20 standard and their transfer
/transferFrom
function return void
instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.
Non-safe transfers are used in:
DepositBoxERC20.depositERC20
Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value.
For the mainnet contracts, we recommend using OpenZeppelinโs SafeERC20
versions with the safeTransfer
and safeTransferFrom
functions that handle the return value check as well as non-standard-compliant tokens.
#0 - cstrangedk
2022-03-03T22:14:41Z
Reference/duplicate #8, #9, #10, and #12.Disputed as SKALE Chain owner can customize depositbox contract using custom messaging to use safeTransfer functions, if needed.
#1 - GalloDaSballo
2022-06-01T18:18:17Z
Dup of #8
๐ Selected for report: 0x1f8b
Also found by: IllIllI, cmichel, gzeon, kirk-baird
786.4623 USDC - $786.46
Some ERC20 tokens make modifications to their ERC20's transfer
or balanceOf
functions.
One type of these tokens is deflationary tokens that charge a certain fee for every transfer()
or transferFrom()
.
Others are rebasing tokens that increase in value over time like Aave's aTokens (balanceOf
changes over time).
The schains deploy a mirror standard ERC20 contract for all token transfers from other chains.
This standard contract does not have these transfer
modifications which leads to issues:
It seems like the issue with fee-on-transfer tokens could be prevented by only minting the post-fee amount on the receiving chain. Consider computing the actual received token amounts from the transferFrom
call and using this amount in the messageProxy.postOutgoingMessage
call.
A similar issue exists on schains where no clone ERC20 is deployed but the mainnet equivalent token is used which could be a fee-on-transfer token.
#0 - cstrangedk
2022-03-03T22:34:19Z
Again, for any non-standard token standard, the SKALE Chain owner is free to create custom depositbox and tokenManager contracts to extend the functionality to other token types and manually map any customized ERCXX token logic instead of using automaticdeployed Erc20OnChain contracts. This is all covered in docs linked to the audit readme https://docs.skale.network/ima/1.2.x/managing-erc20#_1_reviewmodify_the_token_contract
#1 - GalloDaSballo
2022-06-01T16:50:25Z
Dup of #50
๐ Selected for report: cmichel
5993.4639 USDC - $5,993.46
In the ERC721OnChain
implementation the token owner can set the token's URI using setTokenURI
.
Usually, this is token URI points to data defining the NFT (attributes, images, etc.).
It's usually set by the contract owner.
A user that owns an NFT can just spoof any other NFT data by changing the token URI to any of the other NFTs.
Disallow the owner of an NFT to change its token URI
#0 - DimaStebaev
2022-03-11T10:36:29Z
ERC721OnChain
is a default implementation. If the token is sensitive to URI change SKALE chain owner can use another one.
Not all ERC721 require that URI can't be changed.
#1 - GalloDaSballo
2022-06-01T18:26:40Z
Because the argument for this being a setting can be made I am excluding a high severity.
However the code was brought into scope, the implementation under scrutiny does allow the owner to change the URI which is a known admin privilege.
For those reasons I believe the finding to be valid and of medium severity
449.8419 USDC - $449.84
https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/schain/MessageProxyForSchain.sol#L223 https://github.com/skalenetwork/ima-c4-audit/blob/11d6a6ae5bf16af552edd75183791375e501915f/contracts/MessageProxy.sol#L425
The postIncomingMessages
function iterates over all messages
and calls _callReceiverContract
with a counter value of startingCounter + 1
for all of them.
It should be startingCounter + i
.
If a call fails the PostMessageError
event is emitted with startingCounter + 1
instead of the actual counter.
I'm not sure how the nodes handle failed messages but I assume they would undo/refund the original transaction based on these PostMessageError
events that use the counter
to identify a message.
This could then lead to loss of funds. Imagine I post 2 messages:
The first message will revert but emit that the second message reverted. I receive the 1000 USDC on the receiving chain and also get a refund of 1000 USDC, stealing funds.
- _callReceiverContract(fromChainHash, messages[i], startingCounter + 1); + _callReceiverContract(fromChainHash, messages[i], startingCounter + i);
#0 - cstrangedk
2022-03-03T22:56:18Z
Acknowledged this is a bug, but no stealing of funds are possible as this only relates to PostMessageError event emitted with the wrong counter. Suggest: 0 (Non-critical)
#1 - GalloDaSballo
2022-06-01T16:10:15Z
@cstrangedk it seems like the warden could: PostMessage([transferUSDCMessage, revertMessage])
Which will cause the first message to go through and the second one to trigger a Error Event (no revert as you have try/catch)
Are the events used to generate messages, or is you disagreement because the events are not used for any validator / message posting logic ?
#2 - cstrangedk
2022-06-01T19:39:41Z
@GalloDaSballo event error has no impact, it simply emits an error with no further logic/consequence.
#3 - GalloDaSballo
2022-06-01T20:02:30Z
Given the context given by the sponsor, the finding is valid and informational in nature, as such I'll downgrade to non-critical
#4 - JeeberC4
2022-06-01T20:45:51Z
Creating QA Report for warden as judge has downgraded. Preserving original title: Wrong counter in PostMessageError events