code4rena-2024-08-chakra-m07
[M-07] Permanent loss of user tokens on both chains if BurnUnlock mode fails because of flawed burning pattern
Summary
BurnUnlock 모드일 때, 브릿지를 요청하면 토큰을 바로 소각한다. 목적지에서 브릿지에 실패한다면 브릿지 요청했던 토큰을 돌려줄 수 없다.
Keyword
bridge, cross chain, business logic vul
Vulnerability
- solidity/handler/contracts/ChakraSettlementHandler.sol#L123-L124
- solidity/handler/contracts/ChakraSettlementHandler.sol#L325-L329
- solidity/handler/contracts/ChakraSettlementHandler.sol#L383-L389
- solidity/handler/contracts/ChakraSettlementHandler.sol#L129-L130
- solidity/handler/contracts/ChakraSettlementHandler.sol#L344-L348
브릿지에 MintBurn 모드와 BurnUnlock 모드가 있다.
- MintBurn 모드: 토큰은 출발지 체인에서 소각되고 목적지 체인에서 발행되어야 한다.
- BurnUnlock 모드: 토큰은 출발지 체인에서 소각되고 목적지 체인에서 잠금이 해제되어야 한다.
MintBurn 모드는 브릿지 요청 시 토큰을 일단 락해둔다. 이후 목적지에서 callback이 호출되면 잠겨있는 토큰을 소각한다.
function cross_chain_erc20_settlement(
string memory to_chain,
uint256 to_handler,
uint256 to_token,
uint256 to,
uint256 amount
) external {
require(amount > 0, "Amount must be greater than 0");
require(to != 0, "Invalid to address");
require(to_handler != 0, "Invalid to handler address");
require(to_token != 0, "Invalid to token address");
if (mode == SettlementMode.MintBurn) {
@> _erc20_lock(msg.sender, address(this), amount);
} else if (mode == SettlementMode.LockUnlock) {
_erc20_lock(msg.sender, address(this), amount);
} else if (mode == SettlementMode.LockMint) {
_erc20_lock(msg.sender, address(this), amount);
} else if (mode == SettlementMode.BurnUnlock) {
_erc20_burn(msg.sender, amount);
}
...
}
function receive_cross_chain_callback(
uint256 txid,
string memory from_chain,
uint256 from_handler,
CrossChainMsgStatus status,
uint8 /* sign_type */, // validators signature type / multisig or bls sr25519
bytes calldata /* signatures */
) external onlySettlement returns (bool) {
...
if (status == CrossChainMsgStatus.Success) {
if (mode == SettlementMode.MintBurn) {
@> _erc20_burn(address(this), create_cross_txs[txid].amount);
}
create_cross_txs[txid].status = CrossChainTxStatus.Settled;
}
if (status == CrossChainMsgStatus.Failed) {
create_cross_txs[txid].status = CrossChainTxStatus.Failed;
}
return true;
}하지만 BurnUnlock 모드의 요청이 목적지 체인에서 실패할 경우(ex. 검증자 서명이 부족해서), 사용자의 토큰이 양쪽 체인에서 모두 사라진다. 출발지에서 이미 토큰을 소각해버렸기 때문에 돌려받을 수 없다.
function cross_chain_erc20_settlement(
string memory to_chain,
uint256 to_handler,
uint256 to_token,
uint256 to,
uint256 amount
) external {
require(amount > 0, "Amount must be greater than 0");
require(to != 0, "Invalid to address");
require(to_handler != 0, "Invalid to handler address");
require(to_token != 0, "Invalid to token address");
if (mode == SettlementMode.MintBurn) {
_erc20_lock(msg.sender, address(this), amount);
} else if (mode == SettlementMode.LockUnlock) {
_erc20_lock(msg.sender, address(this), amount);
} else if (mode == SettlementMode.LockMint) {
_erc20_lock(msg.sender, address(this), amount);
} else if (mode == SettlementMode.BurnUnlock) {
@> _erc20_burn(msg.sender, amount);
}
{
// Increment nonce for the sender
nonce_manager[msg.sender] += 1;
}
...
}
function receive_cross_chain_callback(
uint256 txid,
string memory from_chain,
uint256 from_handler,
CrossChainMsgStatus status,
uint8 /* sign_type */, // validators signature type / multisig or bls sr25519
bytes calldata /* signatures */
) external onlySettlement returns (bool) {
// from_handler need in whitelist
if (is_valid_handler(from_chain, from_handler) == false) {
return false;
}
require(
create_cross_txs[txid].status == CrossChainTxStatus.Pending,
"invalid CrossChainTxStatus"
);
if (status == CrossChainMsgStatus.Success) {
if (mode == SettlementMode.MintBurn) {
_erc20_burn(address(this), create_cross_txs[txid].amount);
}
create_cross_txs[txid].status = CrossChainTxStatus.Settled;
}
@> if (status == CrossChainMsgStatus.Failed) {
create_cross_txs[txid].status = CrossChainTxStatus.Failed;
}
return true;
}Impact
목적지 체인에서 모종의 이유로 브릿지에 실패했을 때, 토큰을 돌려받을 수 없다.
Mitigation
BurnUnlock 모드인 토큰을 브릿지 요청할 때, 토큰을 일단 lock 해둔다. 이후 callback에서 성공 메시지를 받으면 토큰을 소각한다. callback에서 실패 메시지를 받으면 lock해둔 토큰을 풀어준다.
function cross_chain_erc20_settlement(
string memory to_chain,
uint256 to_handler,
uint256 to_token,
uint256 to,
uint256 amount
) external {
// ...
if (mode == SettlementMode.BurnUnlock) {
+ _erc20_lock(msg.sender, address(this), amount);
- _erc20_burn(msg.sender, address(this), amount);
}
// ...
}function receive_cross_chain_callback(
uint256 txid,
string memory from_chain,
uint256 from_handler,
CrossChainMsgStatus status,
uint8 /* sign_type */,
bytes calldata /* signatures */
) external onlySettlement returns (bool) {
// ...
if (status == CrossChainMsgStatus.Success) {
+ if (mode == SettlementMode.BurnUnlock) {
+ _erc20_burn(address(this), create_cross_txs[txid].amount);
+ }
create_cross_txs[txid].status = CrossChainTxStatus.Settled;
if (status == CrossChainMsgStatus.Failed) {
+ if (mode == SettlementMode.BurnUnlock) {
+ _erc20_unlock(create_cross_txs[txid].from, create_cross_txs[txid].amount);
+ }
create_cross_txs[txid].status = CrossChainTxStatus.Failed;
}
// ...
}tags: bughunting, chakra, smart contract, starknet, cairo, bridge, cross chain, severity medium, business-logic-vul