Solidity Cross Chain Security
Cross-chain security is the set of rules that stops a bridge from minting tokens nobody locked. A quorum rule is one such rule, and it means a fixed number of relayers must agree before any mint happens.
A non-relayer approval reverts
Only accounts in the relayer set may approve a mint. A stranger approval fails before anything is recorded.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract QuorumMint {
mapping(address => bool) public isRelayer;
mapping(bytes32 => mapping(address => bool)) public approved;
mapping(bytes32 => uint256) public approvalCount;
mapping(bytes32 => bool) public executed;
mapping(address => uint256) public balances;
uint256 public totalSupply;
uint256 public quorum;
constructor(address first, address second, address third, uint256 quorumNeeded) {
isRelayer[first] = true;
isRelayer[second] = true;
isRelayer[third] = true;
quorum = quorumNeeded;
}
function approveMint(address to, uint256 amount) public {
require(isRelayer[msg.sender], "not a relayer");
bytes32 id = keccak256(abi.encode(to, amount));
require(!approved[id][msg.sender], "already approved");
require(!executed[id], "already executed");
approved[id][msg.sender] = true;
approvalCount[id] += 1;
if (approvalCount[id] >= quorum) {
executed[id] = true;
balances[to] += amount;
totalSupply += amount;
}
}
}
One approval does not mint
With a quorum of two, the first relayer approval only records one vote. No tokens exist yet after that single approval.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract QuorumMint {
mapping(address => bool) public isRelayer;
mapping(bytes32 => mapping(address => bool)) public approved;
mapping(bytes32 => uint256) public approvalCount;
mapping(bytes32 => bool) public executed;
mapping(address => uint256) public balances;
uint256 public totalSupply;
uint256 public quorum;
constructor(address first, address second, address third, uint256 quorumNeeded) {
isRelayer[first] = true;
isRelayer[second] = true;
isRelayer[third] = true;
quorum = quorumNeeded;
}
function approveMint(address to, uint256 amount) public {
require(isRelayer[msg.sender], "not a relayer");
bytes32 id = keccak256(abi.encode(to, amount));
require(!approved[id][msg.sender], "already approved");
require(!executed[id], "already executed");
approved[id][msg.sender] = true;
approvalCount[id] += 1;
if (approvalCount[id] >= quorum) {
executed[id] = true;
balances[to] += amount;
totalSupply += amount;
}
}
}
A second approval mints
The second relayer approval reaches the quorum of two, so the mint runs. The recipient balance and the supply both read 80.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract QuorumMint {
mapping(address => bool) public isRelayer;
mapping(bytes32 => mapping(address => bool)) public approved;
mapping(bytes32 => uint256) public approvalCount;
mapping(bytes32 => bool) public executed;
mapping(address => uint256) public balances;
uint256 public totalSupply;
uint256 public quorum;
constructor(address first, address second, address third, uint256 quorumNeeded) {
isRelayer[first] = true;
isRelayer[second] = true;
isRelayer[third] = true;
quorum = quorumNeeded;
}
function approveMint(address to, uint256 amount) public {
require(isRelayer[msg.sender], "not a relayer");
bytes32 id = keccak256(abi.encode(to, amount));
require(!approved[id][msg.sender], "already approved");
require(!executed[id], "already executed");
approved[id][msg.sender] = true;
approvalCount[id] += 1;
if (approvalCount[id] >= quorum) {
executed[id] = true;
balances[to] += amount;
totalSupply += amount;
}
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract QuorumMint {
mapping(address => bool) public isRelayer;
mapping(bytes32 => mapping(address => bool)) public approved;
mapping(bytes32 => uint256) public approvalCount;
mapping(bytes32 => bool) public executed;
mapping(address => uint256) public balances;
uint256 public totalSupply;
uint256 public quorum;
constructor(address first, address second, address third, uint256 quorumNeeded) {
isRelayer[first] = true;
isRelayer[second] = true;
isRelayer[third] = true;
quorum = quorumNeeded;
}
function approveMint(address to, uint256 amount) public {
require(isRelayer[msg.sender], "not a relayer");
bytes32 id = keccak256(abi.encode(to, amount));
require(!approved[id][msg.sender], "already approved");
require(!executed[id], "already executed");
approved[id][msg.sender] = true;
approvalCount[id] += 1;
if (approvalCount[id] >= quorum) {
executed[id] = true;
balances[to] += amount;
totalSupply += amount;
}
}
}
Key points
- Cross-chain security: Cross-chain security is the set of rules that stops a bridge from minting tokens nobody locked.
- Quorum: A quorum is a fixed number of approvals that must be reached before an action happens.
- Relayer set: A relayer set is the group of trusted accounts allowed to approve a mint.
- Unbacked mint: An unbacked mint is a mint with no locked value behind it.
- Compromised relayer: A compromised relayer is a trusted account an attacker controls.
- Restatement: A bridge stays safe when no single relayer can mint alone and a fixed quorum must agree first.
In this example, the first relayer approves a mint of 50, the same relayer trying again fails, and the second relayer approval reaches the quorum. Only then do the 50 tokens appear.
Note
A quorum of two out of three still fails if two relayers are compromised together, so the quorum size is a trade-off, not a guarantee.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A bridge needs a quorum of two relayers. One relayer has approved a mint. What is true?