code4rena-2024-08-axelar-h02
[H-02] Can block bridge or limit the bridgeable amount by initializing the ITSHub balance of the original chain
Summary
브릿지를 중개하는 체인에서 토큰 잔고를 추적한다. 이는 원본 체인의 잔고는 (토큰이 추가로 민팅될 수 있어 가변적이므로) 추적하지 않는다. 그런데 원본 체인의 잔고 정보를 초기화하는 방법이 있었고, 이를 초기화하면 잔고가 부족하여 브릿지가 더이상 기능하지 않게 되었다.
Keyword
bridge, lack of input validation, dos
Vulnerability
- interchain-token-service/contracts/InterchainTokenFactory.sol#L176
- interchain-token-service/contracts/InterchainTokenService.sol#L342
- interchain-token-service/contracts/InterchainTokenFactory.sol#L269
ITSHub 는 원본 토큰이 있는 원본 체인에서의 잔고는 추적하지 않는다. 오로지 remote deploy 를 했을 때 remote chain 에서의 잔고만을 추적한다. 원본 체인에서는 보통 minter 를 등록하여 토큰을 발행할 수 있게 하기 때문에 잔고 추적이 어렵기 때문이다.
fn apply_balance_tracking(
storage: &mut dyn Storage,
source_chain: ChainName,
destination_chain: ChainName,
message: &ItsMessage,
) -> Result<(), Error> {
match message {
ItsMessage::InterchainTransfer {
token_id, amount, ..
} => {
// Update the balance on the source chain
@> update_token_balance(
storage,
token_id.clone(),
source_chain.clone(),
*amount,
false,
)
@> .change_context_lazy(|| Error::BalanceUpdateFailed(source_chain, token_id.clone()))?;
// Update the balance on the destination chain
@> update_token_balance(
storage,
token_id.clone(),
destination_chain.clone(),
*amount,
true,
)
@> .change_context_lazy(|| {
Error::BalanceUpdateFailed(destination_chain, token_id.clone())
})?
}
// Start balance tracking for the token on the destination chain when a token deployment is seen
// No invariants can be assumed on the source since the token might pre-exist on the source chain
ItsMessage::DeployInterchainToken { token_id, .. } => {
@> start_token_balance(storage, token_id.clone(), destination_chain.clone(), true)
.change_context(Error::InvalidStoreAccess)?
}
...
};
Ok(())
}토큰을 새로 remote deploy 했을 때만 목적지 체인에서의 balance 를 0으로 초기화한다. 그리고 토큰이 이동할 때는 잔고 데이터가 초기화된 체인의 잔고만 업데이트 한다. 정상적인 경우 원본 체인의 잔고는 추적하지 않는다.
pub fn start_token_balance(
storage: &mut dyn Storage,
token_id: TokenId,
chain: ChainName,
track_balance: bool,
) -> Result<(), Error> {
let key = TokenChainPair { token_id, chain };
match TOKEN_BALANCES.may_load(storage, key.clone())? {
None => {
let initial_balance = if track_balance {
@> TokenBalance::Tracked(Uint256::zero())
} else {
TokenBalance::Untracked
};
TOKEN_BALANCES
.save(storage, key, &initial_balance)?
.then(Ok)
}
Some(_) => Err(Error::TokenAlreadyRegistered {
token_id: key.token_id,
chain: key.chain,
}),
}
}
pub fn update_token_balance(
storage: &mut dyn Storage,
token_id: TokenId,
chain: ChainName,
amount: Uint256,
is_deposit: bool,
) -> Result<(), Error> {
let key = TokenChainPair { token_id, chain };
let token_balance = TOKEN_BALANCES.may_load(storage, key.clone())?;
match token_balance {
Some(TokenBalance::Tracked(balance)) => {
let token_balance = if is_deposit {
balance
@> .checked_add(amount)
.map_err(|_| Error::MissingConfig)?
} else {
balance
@> .checked_sub(amount)
.map_err(|_| Error::MissingConfig)?
}
.then(TokenBalance::Tracked);
TOKEN_BALANCES.save(storage, key.clone(), &token_balance)?;
}
@> Some(_) | None => (),
}
Ok(())
}
그런데 원본 체인의 ITSHub 에서의 잔고를 초기화할 수 있는 방법이 있다. 잔고를 초기화할 때는 실제로 원본 체인에 민팅되어 있는 토큰의 양이 아니라 0으로 초기화된다. 따라서 원본 체인의 ITSHub에서의 잔고를 초기화하면 언더플로우로 인해 ITSHub.update_token_balance 가 실패, 더이상 원본 체인에서 토큰을 다른 체인으로 보낼 수 없게 되거나 브릿지 가능한 양에 한도가 생긴다.
원본 체인의 ITSHub 잔고를 초기화할 수 있는 것은 InterchainTokenFactory.deployRemoteInterchainToken 에서 destinationChain 파라미터가 이 토큰의 원본 체인인지는 확인하지 않기 때문이다. InterchainTokenService.deployInterchainToken 를 직접 호출하여 토큰을 배포할 때에도 동일한 문제가 있다. 이로 인해 리모트 체인에서 원본 체인으로 동일한 tokenId 의 토큰 배포 요청이 가능하다. 원본 체인에는 이미 tokenId를 사용하는 토큰이 있으므로 GMP를 목적지 체인에 approve 하는 것까지는 가능하지만, execute 할 시 트랜잭션이 revert 된다. 하지만 ITSHub balance 초기화는 GMP가 목적지 체인으로 전달되는 과정에 이루어지므로 목적지 체인에서 트랜잭션이 실패하는지는 상관이 없다. 또한 GMP 실행에 실패하더라도 초기화된 balance는 삭제되지 않는다.
InterchainTokenFactory.deployRemoteCanonicalInterchainToken 를 통해 canonical interchain token을 배포할 때도 동일한 문제가 있으며 가장 크리티컬한 심각도를 가진다. Canonical token의 tokenId는 원본 토큰의 주소에 의해 결정되며 호출자의 주소는 영향을 끼치지 않는다. 따라서 누구나 InterchainTokenFactory.deployRemoteCanonicalInterchainToken 를 호출하여 특정 Canonical token을 배포할 수 있다. 이는 즉 누구나 특정 Canonical token이 원본 체인에서 브릿지되어 나오는 것을 막을 수 있음을 의미한다. Canonical token은 한 번만 등록할 수 있으므로 이 토큰은 더이상 Axelar 브릿지를 정상적으로 이용할 수 없게 된다.
원본 체인에 ITSHub 잔고를 초기화하면 어떻게 되는지 예를 들어보자. A, B, C 체인이 있고, A가 원본 체인, B, C는 브릿지가 연동된 체인이라고 하자. 다음은 ITSHub의 잔액이다.
- A: None
- B: 100
- C: 50
이때 취약점을 이용하여 A 체인의 잔고를 초기화했다.
- A: 0
- B: 100
- C: 50
B 또는 C 체인에서 A 체인에 브릿지하기 전까지는 A체인에서 더이상 토큰을 꺼낼 수 없다. 또한 앞으로는 최대 150만큼의 토큰만 브릿지 사이를 이동할 수 있다.
Impact
토큰 배포 계정이 Operator나 FlowLimiter 권한이 없어도 자신이 배포했던 토큰 브릿지를 영구적으로 막거나 브릿지 가능한 양을 제한할 수 있다. 특히 Canonical token은 기존 토큰 배포자가 아닌 아무나가 공격할 수 있다.
Mitigation
InterchainTokenFactory.deployRemoteInterchainToken에서destinationChain이originalChainName와 동일한지 확인하여 원본 체인에 remote deploy 를 요청할 수 없도록 한다.
function deployRemoteInterchainToken(
string calldata originalChainName,
bytes32 salt,
address minter,
string memory destinationChain,
uint256 gasValue
) external payable returns (bytes32 tokenId) {
string memory tokenName;
string memory tokenSymbol;
uint8 tokenDecimals;
bytes memory minter_ = new bytes(0);
{
bytes32 chainNameHash_;
if (bytes(originalChainName).length == 0) {
chainNameHash_ = chainNameHash;
} else {
chainNameHash_ = keccak256(bytes(originalChainName));
}
+ require(chainNameHash_ != keccak256(bytes(destinationChain)), "Cannot remote deploy on original chain");
address sender = msg.sender;
salt = interchainTokenSalt(chainNameHash_, sender, salt);
tokenId = interchainTokenService.interchainTokenId(TOKEN_FACTORY_DEPLOYER, salt);
IInterchainToken token = IInterchainToken(interchainTokenService.interchainTokenAddress(tokenId));
tokenName = token.name();
tokenSymbol = token.symbol();
tokenDecimals = token.decimals();
if (minter != address(0)) {
if (!token.isMinter(minter)) revert NotMinter(minter);
minter_ = minter.toBytes();
}
}
tokenId = _deployInterchainToken(salt, destinationChain, tokenName, tokenSymbol, tokenDecimals, minter_, gasValue);
}InterchainTokenFactory.deployRemoteCanonicalInterchainToken도 마찬가지로originalChainName와destinationChain를 확인하여 원본 체인에 remote deploy 를 요청할 수 없도록 한다.InterchainTokenService.deployInterchainToken를 직접 호출하여 배포하는 경우 원본 체인과 목적지 체인이 같은 지 확인할 수 없다. ITSHub에 각 tokenId 의 원본 체인에 대한 정보를 저장하고, 원본 체인의 잔고는 초기화하지 않도록 한다.
tags: bughunting, axelar, smart contract, solidity, solo issue, rust, cosmwasm, bridge, lack-of-input-validation-vul, dos, severity high