Platform: Code4rena
Start Date: 18/02/2022
End Date: 03/03/2022
Period: 14 days
Status: Completed
Reporters: itsmetechjay, liveactionllama
Pot Size: $125,000 USDC
Participants: 24
Reporters: itsmetechjay, liveactionllama
Judge: GalloDaSballo
Id: 88
League: ETH
Auditor per page
The SKALE core team is extremely excited to present the IMA Bridge to the C4 community for audit. The core team will be ready to answer all your questions help with any onboarding, and clarify anything that may be confusing.
Best of luck!
SKALE Core team
Discord handles:
Is located https://github.com/skalenetwork/ima-c4-audit
If you prefer a video walk through of SKALE Network, IMA Bridge contracts and logic, https://youtu.be/BcHLJ9oFgMc
Contract Name | Lines of Code | Lines of Comment |
---|---|---|
MessageProxy.sol | 175 | 313 |
Messages.sol | 177 | 368 |
mainnet/CommunityPool.sol | 70 | 128 |
mainnet/DepositBox.sol | 48 | 55 |
mainnet/Linker.sol | 82 | 96 |
mainnet/MessageProxyForMainnet.sol | 147 | 204 |
mainnet/SkaleManagerClient.sol | 33 | 32 |
mainnet/Twin.sol | 49 | 46 |
mainnet/DepositBoxes/DepositBoxEth.sol | 93 | 128 |
mainnet/DepositBoxes/DepositBoxERC20.sol | 135 | 229 |
mainnet/DepositBoxes/DepositBoxERC721.sol | 128 | 203 |
mainnet/DepositBoxes/DepositBoxERC1155.sol | 157 | 386 |
schain/CommunityLocker.sol | 113 | 113 |
schain/KeyStorage.sol | 46 | 67 |
schain/MessageProxyForSchain.sol | 160 | 229 |
schain/TokenManager.sol | 129 | 109 |
schain/TokenManagerLinker.sol | 86 | 87 |
schain/bls/FieldOperations.sol | 150 | 191 |
schain/bls/Precompiled.sol | 31 | 39 |
schain/bls/SkaleVerifier.sol | 30 | 69 |
schain/TokenManagers/TokenManagerERC20.sol | 112 | 297 |
schain/TokenManagers/TokenManagerERC721.sol | 104 | 231 |
schain/TokenManagers/TokenManagerERC1155.sol | 138 | 449 |
schain/tokens/EthErc20.sol | 46 | 28 |
schain/tokens/ERC20OnChain.sol | 34 | 22 |
schain/tokens/ERC721OnChain.sol | 54 | 64 |
schain/tokens/ERC1155OnChain.sol | 46 | 51 |
extensions/ERC721ReferenceMintAndMetadataMainnet.sol | 21 | 54 |
extensions/ERC721ReferenceMintAndMetadataSchain.sol | 21 | 43 |
extensions/interfaces/MessageProxyClient.sol | 19 | 12 |
extensions/interfaces/MessageSender.sol | 19 | 11 |
extensions/interfaces/MessageReceiver.sol | 20 | 4 |
thirdparty/ERC1155ReceiverUpgradeableWithoutGap.sol | 6 | 25 |
Total | 2732 | 4458 |
NOTE: contracts in the extensions/*
folder are example contracts demonstrating how a developer may expand the IMA bridge with any arbitrary logic with MessageProxy.
The SKALE Network is an Ethereum-native multichain network, where dApps run on dApp-specific SKALE chains. SKALE Manager contracts orchestrate the entire network, its chains, and validators. SKALE Manager is deployed and operating on the Ethereum mainnet.
Each SKALE chain is supported by 16 randomly selected nodes in the SKALE Network. Each node runs SKALE software and communicates with other SKALE chains and Ethereum.
SKALE chains are deployed and operated by SKALE chain owners - which may be a dapp developer or community. Owners may set deployment permissions to certain accounts, and may set other configuration options for their chain or IMA bridge that is best fit for their dapp and community. Dapps that run on SKALE chains are provisioned bandwidth such that as an owner you are not sharing bandwidth and resources with other dapps like Ethereum. SKALE chain serves the needs for your dapp and community, and if say SKALE chain run by another owner should become overloaded, it will not affect your SKALE chain.
SKALE chains operate in a gas-free environment using a native gas token called sFUEL. sFUEL has no economic value and is allocated from the SKALE chain owner (SKALE chain owner is a dApp developer or community who operates a dApp-specific SKALE chain). Owners are free to use various methods to distribute sFUEL to end-users, either by onboarding, faucet, or allowing end-users to run a small PoW script. SKALE chain gas serves a way to meter or limit transaction usage on the SKALE chain to prevent malicious execution for DDoS attacks.
The IMA Bridge is the native bridge for all SKALE chains: enabling messages to be sent between Ethereum and SKALE chains and between any two SKALE chains.
Unlink other bridges, IMA bridge is designed to be customized, modified and tailored for any SKALE chain by the SKALE chain owner - who is a dapp developer or community who controls a SKALE chain. The IMA bridge part that is deployed to Ethereum Mainnet is designed to connect to any number of SKALE chains, and supports certain customizations.
The IMA Bridge consists of four parts:
A Dapp Developer (say fictitious SWAP DAO) desires a "gas-free" platform for its DEX. SWAP DAO requests a SKALE chain from the Ethereum network and deposits SKL tokens to lease a SKALE chain. The SKALE chain is created from SKALE Manager and IMA SKALE chain contracts are pre-deployed on this chain. SWAP DAO can then customize the IMA bridge settings and permission system, and designate SWAP DAO's Gnosis SAFE multisig as the owner.
SWAP DAO, being a stablecoin-only DEX, decides only to allow specific ERC20 stablecoins. SWAP DAO, with whitelist enabled
, adds the stablecoin contracts to IMA mainnet, designating its SKALE chain name. https://docs.skale.network/ima/1.2.x/managing-erc20#_3_register_ethereum_mainnet_contract_to_ima.
SWAP DAO enables automatic deployment, such that any time a user first deposits any one of the whitelisted stablecoins, a token clone (ERC20OnChain.sol) is created and mapped/linked. Any subsequent deposit of this token by any other user will use this mapping. Once a token is linked to its Ethereum mainnet contract, it cannot be relinked.
SWAP DAO wishes to improve upon the "out-of-the-box" or default safe exit process. The default safe exit process requires end-users to first to deposit ETH into a CommunityPool. This CommunityPool is used to reimburse SKALE nodes that submit the exit transaction to Ethereum. Because of Ethereum network gas volatility, the deposit required for safe exits is more than the exit transaction itself. SWAP DAO wishes to subsidize the exit process from a specific wallet, for all end-users. SWAP DAO registers extra contracts on Ethereum and deploys new contracts such that each user exit pulls from a single wallet on Ethereum.
A dApp developer (say fictitious ETHDog) desires a "gas-free" platform for its Play 2 Earn game where many different types of NFT contracts and collections are generated. As in the above example, ETHDog has provisioned a SKALE chain. ETHDog wishes to 1) send NFT tokens earned in the game to another NFT game ETHCat on the "eth-cat" SKALE chain, and 2) allow "FISH" tokens earned in the game to bridge to "SWAP_DAO", where SWAP_DAO has a FISH/USDC pair deployed on their SKALE chain.
Being a game supporting only one FISH erc20, token, ETHDog and SWAP DAO work together to deploy the FISH token on eth-dog chain, and a token clone on swap-dao chain. Since whitelist is enabled by default, ETHDog manually maps the FISH token to it's clone on swap-dao.
To send NFTs to eth-cat chain, and since there are many nft contracts, eth-dog owner disables the whitelist, and enables automatic deployment, enabling any NFT contract to be automatically mapped and cloned on the eth-cat chain. eth-dog then uses the transferToSchainERC721
to develop the transfer logic in the backend of the UI. When the end-user conducts a transfer to eth-cat, the ERC721 and tokenURI are cloned over to eth-cat for free, and transfers complete within seconds.
You need nodejs 14 or later and yarn to be installed on your machine
clone the repo
run
yarn install
go to proxy
folder
cd proxy
To run all unit tests run
yarn test
If you want to run any particular test (for example MessageProxy.ts
) use
npx hardhat test test/MessageProxy.ts
Developer setup and usage is covered in detail here: https://docs.skale.network
👀 current audit scope covers IMA version v1.2.x https://docs.skale.network/ima/1.2.x/ (be sure the version selector on the top right reads 1.2.x Preview
)
Getting Started https://docs.skale.network/ima/1.2.x/getting-started
Diagramed flows https://docs.skale.network/ima/1.2.x/flows
SKALE chain owners can setup other contracts using the message proxy system. This allows developers to expand beyond the "out-of-the-box" functionalities of Deposit Box and Token Managers to support different flows (Mint NFTs on SKALE first, then send to Ethereum examples are https://github.com/skalenetwork/IMA/tree/develop/proxy/contracts/extensions and https://docs.skale.network/ima/1.2.x/flows#_erc7211155_skale_mint_first), send arbitrary messages, and more!
On a basic level, a message in IMA is a bytes sequence of arbitrary length. It is sent between two smart contracts deployed on different chains. To conduct a transfer, each chain has an extension of an abstract smart contract named MessageProxy
. The MessageProxy
provides 2 main services:
MessageProxy has slightly different implementations for Ethereum mainnet and SKALE chain. They follow the main concept but verify messages in different ways. This will be described later.
When MessageProxy
receives a message to send, it emits an event containing the message. The further transferring is done by an off-chain service named Agent
. It is run on each node (of 16 nodes) of a SKALE chain. Its purpose is to monitor events from a MessageProxy
, verify them and send transactions to target MessageProxy
to perform processing on a destination chain.
Thus the message transfer is done in 3 steps:
MessageProxy
emits an event on a source chain.Agents
parse the event, verify it and forward to the destination chain by sending a transaction to the target MessageProxy
.MessageProxy
on the destination chain verifies the message and executes a callback of a receiver smart contract passing the message as a parameter.To allow a user to send a message, MessageProxy
has a function:
function postOutgoingMessage( bytes32 targetChainHash, address targetContract, bytes memory data ) public;
The first parameter is a destination chain. It is a keccak256 hash of SKALE chain name or keccak256("Mainnet")
if it targets to Ethereum.
The second parameter is an address of smart contract that needs to be called on delivery. The smart contract must implement interface IMessageReceiver
.
interface IMessageReceiver { function postMessage( bytes32 schainHash, address sender, bytes calldata data ) external returns (address); }
Besides the message, the target smart contract will be provided with information about the source chain and sender address.
The third parameter of the postOutgoingMessage
function is the message itself.
As a result of the function execution, an OutgoingMessage
event will be emitted. It has the following structure:
event OutgoingMessage( bytes32 indexed dstChainHash, uint256 indexed msgCounter, address indexed srcContract, address dstContract, bytes data );
A msgCounter
field reflects that all messages in IMA are ordered and numbered. Each pair of chains has its own numeration and messages are always delivered in order that they were sent.
For this purpose, MessageProxy
maintains incoming and outgoing message counters for each pair of chains. Initially, both of them are equal to 0. The sending of a new message increments the outgoing message counter of source MessageProxy
. The destination MessageProxy
accepts only the message with msgCounter
value that is equal to its incoming message counter. After processing of the message, it increments the incoming message counter.
After event emitting on Ethereum random Agent
on the target SKALE chain sends a transaction to the MessageProxy
and the function postIncomingMessages
is executed.
function postIncomingMessages( string calldata fromSchainName, uint256 startingCounter, Message[] calldata messages, Signature calldata sign ) external;
Parameters are:
fromSchainName
- source chain
startingCounter
- number of first message in the batch
messages
- batch of messages in sequential order. Each message is a structure
struct Message { address sender; address destinationContract; bytes data; }
representing sender address, receiver address and the message
sign
- BLS signature of parameters. This parameter is very important and allows to guarantee that the function will be executed only if supermajority of nodes in SKALE chain are agreed on the payload values.
After signature verification the destination MessageProxy
calls function postMessage
of target smart contract passing initial message.
The processing of the return message is almost the same except for the gas reimbursement procedure. MessageProxy
on Ethereum measures how much gas was consumed for the message processing and transfers the equivalent amount from the beneficiary account to the node wallet.
For a diagram description of the gas reimbursement procedure, please see the sequence diagram here: https://docs.skale.network/ima/1.2.x/funding-exits#_exit_reimbursement_flow_process
IMA Bridge uses OpenZeppelin's Access Control framework to set roles and permissions. A developer overview of roles and configuration settings is here:
https://docs.skale.network/ima/1.2.x/access-control
SKALE Network is a network of nodes that are run and maintained by different validators. SKALE nodes support running SKALE chains. Each SKALE chain is run on 16 randomly selected nodes and consume a part of node cpu, memory, and storage resources (1/32, 1/8, 1).
SKALE chain creation is an on-chain operation on Ethereum mainnet, conducted in SKALE Manager contracts. After SKALE chain create transaction for SKALE chain "A" is sent, a DKG round between 16 randomly selected nodes is started. During the DKG round, each node should generate and send an encrypted key share and receive and decrypt key shares from other nodes. When all nodes have sent and received all information each node should send a tx that everything is good. After the last transaction is sent, a common BLS public key of SKALE chain "A" would be calculated and stored in SKALE Manager contracts on Ethereum.
After a successful DKG round between nodes, SKALE chain "A" would start working (blockchain is spun up & all services begin operating). Each node has its own BLS private key, and the common BLS public key is stored in SKALE Manager contracts and locally on each node in its configuration. When a new block is mined on a SKALE chain "A" that means that at least 11 of 16 nodes signed this block by BLS private key and this block was verified by common BLS public key of SKALE chain "A".
Also during each IMA transfer (Ethereum Mainnet -> SKALE chain), (SKALE chain -> Ethereum Mainnet) and (SKALE chain -> SKALE chain) the BLS signature is verified. For example:
To read more about DKG & BLS:
FYI: DKG & BLS cryptography was audited.
IMA Bridge
Cryptography contracts (DKG + BLS, and FieldOperations.sol, Precompile.sol, SkaleVerifier.sol)
SKALE chains can support contract sizes greater than sizes allowed on Ethereum (24kB), therefore you will notice some TokenManager contract sizes > 24kB.
IMA Bridge mainnet interaction with SKALE Manager:
IMA Bridge SKALE chain contracts load config file BLS public keys for a SKALE chain using a special Precompile.sol contract.
There is a limit of 1M of gas units for a message processing. If the limit is exceeded the call to external contract is reverted. It is more than enough to call transfer function of a regular ERC20 token or similar but in general case requires a sender to ensure that target smart contract does not consume too many gas. As an example a transferring of big batch of ERC1155 tokens may overflow the limit. In this case the message will be considered as processed but a state of a smart contract on a target chain will not be modified.
In an extraordinary event, a SKALE chain and its Bridge may be killed if both the SKALE chain owner and the IMA mainnet admin role execute a kill process. Once killed, users are able to retrieve locked deposits.
Exits are rate limited to a default 5 minutes per message, and can be set by the CONSTANT_SETTER_ROLE
.
Using the default exit process, users must fund their CommunityPool account before conducting an exit with 1,000,000 gas * gas Price in Wei at time of exit transaction submission. The actual exit transaction cost is deducted from this CommunityPool deposit, and users are able to withdraw any unused funds in their CommunityPool. An oracle process to retrieve a more accurate Ethereum gas Price is currently planned to be released. In an extraordinary event of a user not having sufficient funds in the Community Pool, the user is locked from subsequent exits until they refund the Community Pool, and in the meantime the SKALE chain owner wallet is used to reimburse nodes for the exit transaction.
Note that SKALE chain owners may implement a different exit process than the default one described above, by using custom messages with the Message Proxy system.
Everything on the SKALE chain operates in a gas-free environment, so no need for optimizations there (schains/*
). Of course, gas optimization strategies are welcome for IMA contracts on Ethereum mainnet.
The IMA Bridge implements OpenZeppelin's contracts-upgradeable
system. Mainnet contracts are upgradeable by multisig and eventually governance. SKALE chains are upgradeable only by the SKALE chain owner which may be multisig or governance at the discretion of the SKALE chain owner.
We would like wardens to focus on any core functional logic (Ethereum -> SKALE, SKALE -> Ethereum, SKALE -> SKALE), boundary case errors or similar issues which could be utilized by an attacker to steal funds or drain wallets.
Gas optimizations are welcome, please see Gas Optimization Notes.
If wardens are unclear on which areas to look at or which areas are important please feel free to ask in the contest Discord channel.