As of 3 Nov 2026, Fastex has 4 audit reports by 2 security firms on Trustblock; the latest is by CertiK, dated 18 Jan 2024. Those reports list no open critical findings.
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| DECA | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Bahamut Execution and Consensus | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Bahamut security review | Security firm: Hexens | Date: | Scope: Not recorded (see the report) | 1 critical finding, fixed | Open · GitHub |
| Fastex - Token Distribution SC | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 1 |
| High | 2 | 2 | 4 |
| Medium | 1 | 0 | 12 |
| Low | 4 | 4 | 43 |
The report gives no description here.
Open the reportThe creation of a channel by the user requires the `sent` parameter to be set to `true`. Nevertheless, in ERC20 mode, the `sent` parameter is always set to `false`, thereby making it impossible for the user to create a ERC20 channel. ```solidity=103 if (casino.isERC20()) { require(0 == msg.value, 'Error: You cannot deposit in ERC20 mode'); } else { (sent, ) = payable(addr).call{value: msg.value}(""); } require(sent, 'Error: Cannot deposit'); ```
Open the reportThere are no sanity checks that make sure the action[2] bet amount in the function argument is the actual asset amount player put into the channel.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportIn the current implementation of `sync()`, the `Ownerprofit` is not subtracted from the `partners[partnerId_].balanceChange`. As a result, if the owner invokes withdrawOwner before a partner withdraws, the partner may not be able to withdraw their entire assets using withdrawPartnerBank() due to an insufficient balance. ```solidity=255 function sync(uint32 partnerId_, int256 balanceChange_) external { require(partnerExists(partnerId_), 'Error: Partner with this ID does not exist'); if (partners[partnerId_].startTime + MONTH < block.timestamp) { if (0 < partners[partnerId_].balanceChange) { ownerBalance += uint256(partners[partnerId_].balanceChange) * partners[partnerId_].ownerProfitPercentage / 100; } partners[partnerId_].startTime = block.timestamp; partners[partnerId_].balance += partners[partnerId_].balanceChange; partners[partnerId_].balanceChange = 0; } partners[partnerId_].balanceChange += balanceChange_; } ```
Open the reportThe function `sync()` can be called by anyone as it has no access restriction. This enables anyone to call this and change the balance of the partner. ```solidity=255 function sync(uint32 partnerId_, int256 balanceChange_) external { require(partnerExists(partnerId_), 'Error: Partner with this ID does not exist'); if (partners[partnerId_].startTime + MONTH < block.timestamp) { if (0 < partners[partnerId_].balanceChange) { ownerBalance += uint256(partners[partnerId_].balanceChange) * partners[partnerId_].ownerProfitPercentage / 100; } partners[partnerId_].startTime = block.timestamp; partners[partnerId_].balance += partners[partnerId_].balanceChange; partners[partnerId_].balanceChange = 0; } partners[partnerId_].balanceChange += balanceChange_; } ```
Open the reportIn the `startAction()` function of the spin contract, the variable `balanceChange` is calculated by multiplying `action_[2]` with `action_[3]`, which represents the total bet amount. However, when inheriting the spin contract, the `UltraHot`, `BlazingHot`, `DiamondFlash`, `DiamondRush`, `LucyBell20`, `LosApaches`, and `MagicTreasuresOfEgypt` contracts calculate the win variable using only `state_[2]` instead of `state_[2] * state_[3]` in their respective `spinAction()` functions. This difference in the calculation may result in unexpected winning amounts if the value of `state_[3]` is not equal to 1. Furthermore, the `scatWin()` function utilizes `state_[2] * action_[3]`, while the `winCalculation()` function does not consider the multiplication factor. ```solidity=40 function startAction(uint256[] memory state_, uint256[] memory action_, address) virtual override internal view returns (int256, uint256[] memory) { ... int256 balanceChange = maxWin - int256(action_[2] * action_[3]); ... } ``` ```solidity=86 function spinAction(uint256[] memory state_, uint256[] memory rand_, address wheelAddress_) virtual override public view returns (int256, uint256[] memory) { ... win *= state_[2]; ... } ```
Open the reportAs per the process definition, when a user places a bet using the `_startAction()` function, the casino initially transfers the winning amount to the channel. Subsequently, the `serverStep()` function facilitates the transfer of bets and winnings from the channel to the casino, resulting in the deduction of the user's bets. However, the `balanceChange` value returned by the `_startAction()` function corresponds to the bet amount, which is greater than the winnings. Thus, in the event of a loss, the user would end up losing less than expected. ```solidity=123 function _startAction(uint256[] memory state_, uint256[] memory action_) private view returns (int256, uint256[] memory) { assert(1 == action_[0]); require(3 == state_[0] || 0 == state_[0], 'Error: state[0] had wrong value'); int256 balanceChange = 0; require(action_[3] <= height, 'Error: action[3] should not be greater than height'); require(0 != action_[3], 'Error: action[3] should not be 0'); require(0 != action_[4], 'Error: action[4] should not be 0'); require(1 == action_[3] || state_[3] + 1 == action_[3], 'Error: incorrect line jump'); if (1 == action_[3]) { require(int256(action_[2] * action_[1]) <= maxBet, 'Error: bet cannot be more than max bet'); balanceChange = int256(action_[2] * action_[1]); state_[6] = 0; } else { require(state_[1] == action_[1], 'Error: Can not change game column count during game'); balanceChange = int256(state_[6] * state_[1]); } state_[0] = 1; // Change state to current state_[1] = action_[1]; state_[2] = (action_[2] * 95238) / 100000; state_[3] = action_[3]; state_[4] = action_[4]; return(balanceChange, state_); } ``` ```solidity=153 function _serverReply(uint256[] memory state_, uint256[] memory action_) private pure returns (int256, uint256[] memory) { assert(2 == action_[0]); require(1 == state_[0], 'Error: state[0] should not be 1'); require(state_[1] == action_[1], 'Error: state[1] should be equal to action[1]'); int256 balanceChange = 0; if (1 == state_[3]) { balanceChange = -int256((state_[1] + 1) * state_[2]); } else { balanceChange = -int256((state_[1] + 1) * state_[6]); } state_[0] = 2; // Change state to current return(balanceChange, state_); } ```
Open the reportFiles: - `beacon-chain/core/altair/sync_committee.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The incorrect generation of `randByte` allows any active validator to be selected in the sync committee regardless of their effective balances, which does not align with the consensus algorithm. The function `NextSyncCommitte()` is used to select the sync committee members from the active validators. In the current code, Fasttoken implements two algorithms according to the version. ```go=61 func NextSyncCommittee(ctx context.Context, s state.BeaconState) (*ethpb.SyncCommittee, error) { var indices []primitives.ValidatorIndex var err error if s.Version() < version.FastexPhase1 { indices, err = NextSyncCommitteeIndices(ctx, s) } else { indices, err = NextSyncCommitteeIndicesFastexPhase1(ctx, s) } ... ``` If the version is less than the `FastexPhase1`, it uses the custom algorithm that applies the validator power by invoking the function `NextSyncCommitteeIndices()`. On the other hand, if the version is in post `FastexPhase1`, the algorithm inherits the original one from Ethereum Proof of Stake, which is implemented in the function `NextSyncCommitteeIndicesFastexPhase1()`. Both algorithms use the same randomness generation and the same `maxRandomByte` (= 65535). In the function `NextSyncCommitteeIndices()`, the `randomBytes` is generated by two bytes, so `randomBytes` is in the range of 0 and 65535. **NextSyncCommitteeIndices()** ```go=127 b := append(seed[:], bytesutil.Bytes8(uint64(i.Div(16)))...) hash := hashFunc(b) bytes2 := append([]byte{}, hash[i%16], hash[16+i%16]) randomBytes := new(big.Float).SetUint64(uint64(bytesutil.FromBytes2(bytes2))) ``` However, the `randomByte` in the function `NextSyncCommitteeIndicesFastexPhase1()` only has one byte, which is in the range of 0 and 255. In this case, the ratio `randomByte`/`maxRandomByte` is too small which allows almost all validators to be selected regardless of their effective balances. In other words, the effective balance does not affect the chance of a validator to be selected. **NextSyncCommitteeIndicesFastexPhase1()** ```go=200 b := append(seed[:], bytesutil.Bytes8(uint64(i.Div(32)))...) randomByte := hashFunc(b)[i%32] cIndex := indices[sIndex] v, err := s.ValidatorAtIndexReadOnly(cIndex) if err != nil { return nil, err } effectiveBal := v.EffectiveBalance() if effectiveBal*maxRandomByte >= cfg.MaxEffectiveBalance*uint64(randomByte) { cIndices = append(cIndices, cIndex) } ```
Open the reportFiles: - `beacon-chain/rpc/prysm/v1alpha1/validator/proposer_attestations.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The function `filter()` is intended to filter the attestation list into valid and invalid attestations separately, which has different implementations according to different versions. However, there is a logical flaw introduced in lines 91-108 due to the fact that `version.Altair < version.FastexPhase1` (i.e., 1 < 3): ```go=91 } else if st.Version() >= version.Altair { // Use a wrapper here, as go needs strong typing for the function signature. attestationProcessor = func(ctx context.Context, st state.BeaconState, attestation *ethpb.Attestation) (state.BeaconState, error) { totalBalance, err := helpers.TotalActiveBalance(st) if err != nil { return nil, err } return altair.ProcessAttestationNoVerifySignature(ctx, st, attestation, totalBalance) } } else if st.Version() >= version.FastexPhase1 { // Use a wrapper here, as go needs strong typing for the function signature. attestationProcessor = func(ctx context.Context, st state.BeaconState, attestation *ethpb.Attestation) (state.BeaconState, error) { totalBalance, err := helpers.TotalActiveBalance(st) if err != nil { return nil, err } return fastexphase1.ProcessAttestationNoVerifySignature(ctx, st, attestation, totalBalance) } ``` The branch `st.Version() >= version.FastexPhase1` is unreachable because any version not less than `version.Altair` will enter the branch `st.Version() >= version.Altair` in line 91. In this case, if the current version is in the post-FastexPhase1, it will use the function `altair.ProcessAttestationNoVerifySignature()` instead of the function `fastexphase1.ProcessAttestationNoVerifySignature()` as the `attestationProcessor`, which could lead to an unexpected result. For example, different implementations of the function `RewardProposer()` will be invoked.
Open the reportFiles: - `beacon-chain/execution/log_processing.go` - `contracts/deposit/logs.go` Commit: - [`3226f8330911cb8df77e775f0155b335ba771bd8`](https://github.com/fastexlabs/bahamut-consensus/tree/3226f8330911cb8df77e775f0155b335ba771bd8) The `ProcessDepositLog()` function in is responsible for handling the received log from the `eth1` chain and generating the deposit data object. However, there is currently no logic implemented to handle the `Contract` attribute or include it in the deposit data object. **beacon-chain/execution/log_processing.go** ```go=133 depositData := ðpb.Deposit_Data{ Amount: bytesutil.FromBytes8(amount), PublicKey: pubkey, Signature: signature, WithdrawalCredentials: withdrawalCredentials, } ``` **contracts/deposit/logs.go** ```go=11 func UnpackDepositLogData(data []byte) (pubkey, withdrawalCredentials, amount, signature, index []byte, err error) { reader := bytes.NewReader([]byte(DepositContractABI)) contractAbi, err := abi.JSON(reader) if err != nil { return nil, nil, nil, nil, nil, errors.Wrap(err, "unable to generate contract abi") } unpackedLogs, err := contractAbi.Unpack("DepositEvent", data) if err != nil { return nil, nil, nil, nil, nil, errors.Wrap(err, "unable to unpack logs") } return unpackedLogs[0].([]byte), unpackedLogs[1].([]byte), unpackedLogs[2].([]byte), unpackedLogs[3].([]byte), unpackedLogs[4].([]byte), nil } ```
Open the reportFiles: - `core/vm/evm.go` Commit: - [`af75d5f6c6ab5a33f6a1ac86c5c443e7be943cf1`](https://github.com/fastexlabs/bahamut-execution/tree/af75d5f6c6ab5a33f6a1ac86c5c443e7be943cf1) In the execution layer, the invocation of the function `CallCode()` changes the address's activity based on gas usage, one of which is the memory gas usage: **CallCode()** ```go=341 memGas, err := evm.memoryGas(input) if err != nil { return nil, gas, err } if caller.Address() != evm.Origin { memGas = 0 } evm.StateDB.AddActivity(addrCopy, initialGas-contract.Gas-contract.OthersGas+memGas) evm.StateDB.AddActivities(&types.Activity{ Address: addrCopy, Activity: evm.StateDB.GetActivity(addrCopy), DeltaActivity: initialGas - contract.Gas - contract.OthersGas, }) ``` However, an inconsistency occurs when adding the activity to the `evm.StateDB`. In line 349, the added activity is calculated as `initialGas-contract.Gas-contract.OthersGas+memGas`, while the `memGas` is missing in line 353 in the call of `evm.StateDB.AddActivities()`, which only accepts `initialGas - contract.Gas - contract.OthersGas` as an input.
Open the reportFiles: - `beacon-chain/core/blocks/deposit.go` Commit: - [`3226f8330911cb8df77e775f0155b335ba771bd8`](https://github.com/fastexlabs/bahamut-consensus/tree/3226f8330911cb8df77e775f0155b335ba771bd8) If a contract is associated with a previous validator, and a new validator registers the same contract, then the previous owner's contract will be set to a zero address. This creates an issue that any validator can occupy the contract with high activities, thereby gaining more power and increasing their chances of being selected as the block proposer. ```go=192 if contractExist { // Set zero-contract to the old owner of the contract // if the contract is already presented in beacon state. owner, err := beaconState.ValidatorAtIndex(contractOwner) if err != nil { return nil, newValidator, err } newVal := ethpb.CopyValidator(owner) newVal.Contract = params.BeaconConfig().ZeroContract[:] if err := beaconState.UpdateValidatorAtIndex(contractOwner, newVal); err != nil { return nil, newValidator, err } } ``` Moreover, in the new design, a validator owns at most one contract, and the current owner of a contract cannot update the contract because once the validator has been set, then it will not be able to enter the branch to update the contract. ```go=173 contractOwner, contractExist := beaconState.ValidatorIndexByContract(bytesutil.ToBytes20(contract)) index, ok := beaconState.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubKey)) if !ok { ... ``` The auditing team would like to confirm with the Fasttoken team if the existing logic is in accordance with the design.
Open the reportThe report gives no description here.
Open the reportBefore transferring the owner from `address1` to `address2`, ensure that `address1` is the owner and `address2` is not. The checks in the following code are opposite. ```solidity=125 if (TransactionType.ChangeOwner == transaction.transactionType) { require(! isOwner[transaction.address1], 'address1 must be owner'); require(isOwner[transaction.address2], 'address2 cannot be owner'); uint256 index = _ownerIndex(transaction.address1); owners[index] = transaction.address2; isOwner[transaction.address1] = false; isOwner[transaction.address2] = true } ```
Open the reportCurrently, the requirement on the `numberOfRequiredConfirmations` is that it should be greater than 0 and no greater than `_owners.length`. Hypothetically, the `_owners.length` could be 5 or 7 and the `numberOfRequiredConfirmations` could be 1, in which case the contract does not really serve the purpose of a multisig contract.
Open the reportThe function `createChannel()` is called to create a new channel without checking whether `partnerId_` is locked or whether `casino` is locked.
Open the reportAbstract contracts are used as base contracts so that the child contract can inherit and utilize its functions, but contract `ChangingReelsFreeSpinScatterWildCard` is not inherited by other contracts.
Open the reportWhen transferring standard ERC20 deflationary tokens, the input amount may not be equal to the received amount due to the charged transaction fee. As a result, an inconsistency in the amount will occur and the transaction may fail due to the validation checks.
Open the reportFor `minBet`, the check condition is `action_[2] >= minBet`, which ensures that the value of `action_[2]` (presumably representing the bet amount) is greater than or equal to the minimum bet requirement specified by "minBet". This condition guarantees that the bet placed meets or exceeds the minimum amount set by the contract. On the other hand, for `maxBet`, the check condition is `(action_[2] * action_[3]) <= maxBet`, where `action_[2] * action_[3]` represents the total bet amount considering the number of hands played. This condition checks if the total bet amount is less than or equal to the maximum bet limit defined by `maxBet`. It ensures that the combined bet amount, taking into account the number of hands played (represented by `action_[3]`), does not exceed the maximum bet limit. In summary, `minBet` enforces a minimum bet requirement for each individual hand, while `maxBet` limits the total bet amount based on the number of hands played and the maximum bet restriction.
Open the reportIn the `_startAction()` and `_serverReply()` functions, the balance is calculated by multiplying the bet by the coefficient. However, in the `getHiLoResult()` function, the calculation involves multiplying the bet by a coefficient and dividing the result by 100.
Open the reportThe `depositPartnerBank()` function is not allowed to deposit ERC20 tokens, while the `depositPartnerBankERC20()` function is designed specifically for that purpose. However, it's worth noting that the error messages generated by the `require(! isERC20(), 'Error: You can deposit only ERC20 tokens')` and `require(isERC20(), 'Error: Deposit of ERC20 tokens is not allowed')` are contradictory.
Open the reportThe truncation will occur when the `action_[2]` equals 1. If the user deposits a bet of 1, the `state_[2]` equals 0. However, in the `_finishAction()` function, the first calculation of the winning amount is based on `state_[2]`. Once the user selects the correct result and since "state_[2]" is equal to 0, the return value of the `getChoiceResult()` function will be 0 because the parameter `win` is also equal to 0. As a result, the user will be judged to have lost. ```solidity=123 function _startAction(uint256[] memory state_, uint256[] memory action_) private view returns (int256, uint256[] memory) { assert(1 == action_[0]); require(3 == state_[0] || 0 == state_[0], 'Error: state[0] had wrong value'); int256 balanceChange = 0; require(action_[3] <= height, 'Error: action[3] should not be greater than height'); require(0 != action_[3], 'Error: action[3] should not be 0'); require(0 != action_[4], 'Error: action[4] should not be 0'); require(1 == action_[3] || state_[3] + 1 == action_[3], 'Error: incorrect line jump'); if (1 == action_[3]) { require(int256(action_[2] * action_[1]) <= maxBet, 'Error: bet cannot be more than max bet'); balanceChange = int256(action_[2] * action_[1]); state_[6] = 0; } else { require(state_[1] == action_[1], 'Error: Can not change game column count during game'); balanceChange = int256(state_[6] * state_[1]); } state_[0] = 1; // Change state to current state_[1] = action_[1]; state_[2] = (action_[2] * 95238) / 100000; state_[3] = action_[3]; state_[4] = action_[4]; return(balanceChange, state_); } ``` ```solidity=47 function getChoiceResult(uint256 gameType_, uint256 choice_, uint256 lastWin_, uint256 rand_) public view returns (uint256) { require(gameSize > rand_, 'Error: rand too big'); if (choice_ == rand_ + 1) { return (gameType_ + 1) * lastWin_; } return 0; } ```
Open the reportEncoding through `abi.encodePacked()` is used with dynamically-sized inputs controlled by the user. This type of encoding is susceptible to malleability when used with strings, bytes, or other dynamically-sized types. As such, it is not a good choice for signature verification with the input used, since users can change the input and potentially get the same bytes output from `abi.encodePacked`. ```solidity=496 return keccak256(abi.encodePacked('dev', gameAddress_, channelAddress_, gameState_, rand2_, balanceChange_, rand1Sha_, newGameNonce_)); ``` - `abi.encodePacked` is called with dynamically-sized arguments: `"dev"`, `gameState_`, `rand2_` ```solidity=487 uint256[] memory gameState_, ``` - `gameState_` is a dynamically-sized external parameter. ```solidity=488 uint256[] memory rand2_, ``` - `rand2_` is a dynamically-sized external parameter.
Open the reportIn the `serverReply()` function, there is no check for `action_[4]` and `state_[doubleStateIndex]` being zero when the action is `spin`. Additionally, the `finishAction()` function also fails to check for `state_[doubleStateIndex]` being zero.
Open the reportThe **require** can be used to check for conditions and throw an exception if the condition is not met. It is better to provide a string message containing details about the error that will be passed back to the caller.
Open the reportWithin the `spinAction()` function, the `win` variable is divided by `wheel.winMultiply()`. However, in the `getImageDetails()` function, the `retvals` variable is not divided by `wheel.winMultiply()`. If the `wheel.winMultiply()` is not equal to 1, the return values of `spinAction()` and `getImageDetails()` may not match.
Open the reportMalicious hackers may observe the pending transaction which will execute the `addWin/Line/Reel` function, and launch a similar transaction but with the hacker-picked Array to manipulate the gaming output. In the `deploy.js`, the `start()` function does the initialization of all game parameters and adds them to the casino's game list.
Open the reportThe `state_[4]` equals to `win` in the linked code, but the `balanceChange` is `win / wheel.winMultiply()`. In the current setting file `UltraHot.json`, the `winMultiply()` is 1 and the `state_[4]` and `balanceChange` are equal, so this will not introduce problems. However, if the `winMultiply()` is not 1, the `balanceChange` may be less than `state_[4]` and the user will receive less winning amount. ```solidity=111 state_[4] = win; state_[doubleStateIndex] = 0; win /= wheel.winMultiply(); return (int256(win), state_); ```
Open the reportA reentrancy attack can occur when the contract creates a function that makes an external call to another untrusted contract before resolving any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the external call resolved the effects. In the linked code, `ownerBalance` is updated after the external call `payable(address_).call{value: amount_}("")`, which allows the `OWNER_ROLE` to re-enter the contract and make another `WithdrawOwner` call. This allows the `OWNER_ROLE` to drain the ETH balance within the casino contract. ```solidity=114 function withdrawOwner(address to_, uint256 amount_) external onlyRole(OWNER_ROLE) { require(ownerBalance >= amount_, 'Error: Insufficient balance'); require(_getbalance(address(this)) >= amount_, 'Error: Casino not allowed to withdraw that amount'); _transfer(to_, amount_); emit OwnerWithdrawed(to_, amount_); ownerBalance -= amount_; } ```
Open the reportState variables that never change should be declared as `constant` to save gas. ```solidity=17 uint256 public height = 10; ``` - `height` should be declared `constant`. --- ```solidity=18 uint256 public gameSize = 3; ``` - `gameSize` should be declared `constant`.
Open the reportThe `bonusAction()` function is an empty function without any implementation. Once it becomes a game, calling `startAction()` function again after a round will result in an "Error: state[0] had wrong value" error message because `state_[0]` was not updated to `3` in the `finishAction()` function.
Open the reportOne or more state variables are used without being initialized in the constructor. ```solidity=15 uint256 public wildCard; ``` - `wildCard` is never initialized, but used in `ChangingReelsFreeSpinScatterWildCard.spinAction`. --- ```solidity=16 uint256 public scatSymbol; ``` - `scatSymbol` is never initialized, but used in `ChangingReelsFreeSpinScatterWildCard.spinAction`.
Open the reportFiles: - `beacon-chain/core/helpers/validators.go` Commit: - [`3226f8330911cb8df77e775f0155b335ba771bd8`](https://github.com/fastexlabs/bahamut-consensus/tree/3226f8330911cb8df77e775f0155b335ba771bd8) The function `RandomBytes()` is intended to generate a pseudo-random number between 0 and `totalEffectivePower`-1. The `randomNumber` generated in line 424 with index 0 will be overwritten by the for loop in line 430, which can be merged into the for loop and start the index with 0. ```go=419 func RandomBytes(seed [32]byte, totalEffectivePower uint64) uint64 { maxRandomBytes := uint64(1<<64 - 1) hashFunc := hash.CustomSHA256Hasher() hash := hashFunc(append(seed[:], bytesutil.Bytes8(0)...)) randomBytes := hash[:8] randomNumber := bytesutil.FromBytes8(randomBytes) if totalEffectivePower == 0 { return 0 } for i := uint64(1); randomNumber > (maxRandomBytes/totalEffectivePower)*totalEffectivePower; i++ { hash = hashFunc(append(seed[:], bytesutil.Bytes8(i)...)) randomBytes = hash[:8] randomNumber = bytesutil.FromBytes8(randomBytes) } return randomNumber % totalEffectivePower } ```
Open the reportCommit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) MEV bots serves as a tool to frontrun a pending transaction to extract the value. To prevent being frontrun by themselves, MEV bots could use MEV-Boost/Relays as trusted mediator between block producers and block builders, which is an implementation of proposer-builder separation (PBS) built by Flashbots for the Ethereum Proof of Stake. Validators could run MEV-Boost to maximize their staking reward by selling blockspace to an open market of builders. Block proposers could bid on transactions, then builders create blocks that include the most valuable transactions, and validators sign the transactions. Normally, blocker proposers can not see the transactions in the block until they signed the block header, which makes it difficult to frontrun the transactions in the block. To identify transactions for exploit, the validator sent a signed, invalid block to MEV-Boost/Relay, which replied with the transactions that should have been included in that block. With the transactions in the block revealed, the validator could observe the transactions and manipulate the transactions. This critical vulnerability was exploited on April 3rd, 2023, which leads to ~20M asset loss of multiple sandwich bots. The patch has been released on the [`MEV-Boost Relay`](https://github.com/flashbots/mev-boost-relay/pull/330), but it requires the corresponding modification of the client, which has been released in the Prysm v4.0.2, but it is not included in the Prysm v3.2.2. Since the Bahamut is built on the Prysm v3.2.2 and the validators may also run the MEV-Boost/Relay, it is recommended to upgrade to the latest version to ensure the fix work properly. **Reference:** - [`Post mortem`](https://collective.flashbots.net/t/post-mortem-april-3rd-2023-mev-boost-relay-incident-and-related-timing-issue/1540) - [`MEV Bot Incident Analysis`](https://www.certik.com/resources/blog/30h7lDtiv9pJiwloeTPXgW-mev-bot-incident-analysis)
Open the reportFiles: - `beacon-chain/core/blocks/deposit.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The Fasttoken consensus algorithm utilizes the gas usage of contracts that are associated with the validators as activity scores to compute the power of the validators. According to the current codebase, the registration of contracts with validators occurs in the contract `deposit_contract` in the execution layer where the validators are able to deposit the stake as well as the contract address for the registration process. In the current implementation, the validator only needs to pass an address for the registration. The auditing team would like to confirm with the Fasttoken team if the following points are taken into account: 1. The passed address has not been validated that is associated with an existing contract, which means it could be an EOA or a placeholder for future contract deployment. Since some classes of addresses (i.e., vanity addresses) are popular in the blockchain, the validators could register many such addresses. If some contract is deployed in the future with these addresses, the validator will own the activity generated by these contracts. 2. If the passed address comes from an existing contract, it could belong to other deployers and not necessarily be owned by this validator. Considering the blockchain is a dark forest, the contract address registration could also be front run by other validators.
Open the reportFiles: - `contracts/deposit/contract.go` - `contracts/deposit/deposit_contract.sol` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The contract `deposit_contract` serves as the entry point for the validator registration on the execution layer. In the current codebase, the `deposit_contract` has not been modified to align with Fasttoken's new design. Its binding `contract.go` seems to be updated as the ABI is different from the current `deposit_contract`, but the deposit event is not updated, as it does not contain `DeployedAddress` and `DeploymentNonce`: **contracts/deposit/contract.go** ```go=362 type DepositContractDepositEvent struct { Pubkey []byte WithdrawalCredentials []byte Amount []byte Signature []byte Index []byte Raw types.Log // Blockchain specific contextual infos } ``` Additionally, the function `Deposit()` is supposed to be only used for testing as it includes hardcoded address and nonce: **contracts/deposit/contract.go** ```go=269 func (_DepositContract *DepositContractTransactor) Deposit(opts *bind.TransactOpts, pubkey []byte, withdrawal_credentials []byte, signature []byte, deposit_data_root [32]byte) (*types.Transaction, error) { return _DepositContract.contract.Transact( opts, "deposit", pubkey, withdrawal_credentials, signature, deposit_data_root, common.HexToAddress("0x1111111111111111111111111111111111111111"), big.NewInt(1)) } ```
Open the reportFiles: - `beacon-chain/core/fastex-phase1/reward.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The fasttoken introduces a novel proposer base reward calculation based on the validator's power via the activity score associated with the validator's registered contracts. The function `BaseProposerReward()` is used to compute the base proposer reward as follows: $$BPR = \frac{(A+T)*bf}{W * n * gwei},$$ where $A = \sum_{i=1}^{n} ea_i$ is the total effective activities of the $n$ validators and $T$ is the transaction constant gas in the window of 1575 epochs. The $bf$ is the base fee of the block and $W$ is the epoch window size 1575, $n$ is the number of validators and $gwei$ is the constant $10^9$. Therefore, the base proposer reward in each epoch is $BPR * s$, where $s$ (= 32) is the number of slots in an epoch. On the other hand, the total validator base reward per epoch is given by: $$BR_{total} = \frac{B*f}{\sqrt{B}} = f*\sqrt{B},$$ where $B$ is the total active balance, and $f$ is the constant 156. Assume that the current number of validators is the target number 4096, and each of them has an effective balance of $8192 * 10^9$. Then the base reward is $$156 * \sqrt{4096 * 8192 * 10^9} = 0.9 * 10^9 gwei$$. According to the design, $7/8$ of the reward will be distributed to the validators for attestation rewards and participating sync committees. In the Ethereum PoS, the remaining $1/8$ of the reward (i.e., $0.1125 * 10^9$) is granted to the block proposers for proposing blocks. The fasttoken attempts to use the base proposer reward in each epoch (i.e., $32*BPR$) to replace the $1/8$ of the total validator base reward per epoch as the reward to the block proposers. In this case, assume that each block has a half load (15M gas consumed) and the base fee is $100 * gwei$, then the base proposer reward in each epoch is $$32 * \frac{(A+T)*bf}{W * n * gwei} = 32 * \frac{(32 *15M)* 100 * gwei}{4096 * gwei} = 0.395 * 10^9 gwei,$$ which is larger than the $1/8$ of the total validator base reward per epoch ( $0.1125 * 10^9$ ). In this case, the block proposers will be incentivized for their duties. However, the auditing team has the following points that would like to check with the fasttoken team: 1. The base proposer reward $BPR$ depends on the $A+T$. If the block activity is low for a long time, then the $BPR$ could be very small (because it is linear with $A+T$.) compared to the $1/8$ stable reward; 2. If the number of validators increases, then the $BR_{total}$ increases but $BPR$ decreases. Take the max number of validators, 20480 as an example, the $BPR$ will be $1/5$ of the previous one, which is $0.079 * 10^9$ but the $1/8$ of the total validator base reward is $0.25 * 10^9$. In both cases, the block proposer reward based on the new design could be less than the $1/8$ of the total validator base reward in the old design. As a result, the block proposers could possibly be disincentivized to participate in the consensus because the reward in the new design is not predictable and prone to change.
Open the reportFiles: - `beacon-chain/core/helpers/validators.go` - `beacon-chain/core/altair/sync_committee.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) According to the current codebase, the block producer and sync committee member selection inherits the algorithm from the RANDAO randomness generation in the Ethereum Proof of Stake. Before the version `FastexPhase1`, the implementation contains some modifications that use the sigmoid function and validator's power defined by the validator's activity score. In Ethereum Proof of Stake, the selection is performed through a shuffle to make the list of active validators randomly, then for each validator, a random number `rand` is generated between 0 and `MaxRand` to check if the inequality $$\frac{s_i}{s} \geq \frac{rand}{MaxRand}$$ holds, where $s_i$ is the effective balance of the validator $i$ and $s$ is the max effective balance. In the Fasttoken, the same approach is adopted with the following changes in the inequality $$(2\cdot \frac{1}{1+e^{-1.5 \cdot \frac{P_i}{P}}} - 1) \cdot \frac{s_i}{s} \geq \frac{rand}{MaxRand} \cdot 0.62$$ The sigmoid function on the left is used to adjust the effective balance of the validator, where the $P_i$ is the power of the validator $i$ and $P$ is the max power of all the validators.
Open the reportFiles: - `beacon-chain/core/helpers/beacon_committee.go` - `beacon-chain/core/helpers/validators.go` - `beacon-chain/core/altair/sync_committee.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) In both functions `BeaconProposerIndex()` and `precomputeProposerIndices()`, an `if-else` logic is coded to select the function for computing the index of the proposer based on the `Version`. The function `BeaconProposerIndex()` in **beacon-chain/core/helpers/validators.go**: ```go=301 if state.Version() < version.FastexPhase1 { return ComputeProposerIndex(state, indices, seedWithSlotHash) } return ComputeProposerIndexFastexPhase1(state, indices, seedWithSlotHash) ``` The function `precomputeProposerIndices()` in **beacon-chain/core/helpers/beacon_committee.go**: ```go=453 if state.Version() < version.FastexPhase1 { index, err = ComputeProposerIndex(state, activeIndices, seedWithSlotHash) if err != nil { return nil, err } } else { index, err = ComputeProposerIndexFastexPhase1(state, activeIndices, seedWithSlotHash) if err != nil { return nil, err } } proposerIndices[i] = index ``` Based on our understanding, when the version is before the `FastexPhase1`, the consensus client should still be in `PoS` mode, and the logic for calculating the index of the proposer should be the same as in Ethereum. However, in the implementation, the `ComputeProposerIndex()` function takes not only the **effective balance** of the validator but also the **effective activity**, which should be calculated in the `PoSA` mode. The function `ComputeProposerIndex()` in **beacon-chain/core/helpers/validators.go**: ```go=384 // ComputeProposerIndex returns the index sampled by effective balance, which is used to calculate the proposer. func ComputeProposerIndex(bState state.ReadOnlyBeaconState, activeIndices []primitives.ValidatorIndex, seed [32]byte) (primitives.ValidatorIndex, error) { length := uint64(len(activeIndices)) if length == 0 { return 0, errors.New("empty active indices list") } maxRandomByte := new(big.Float).SetUint64(1<<16 - 1) hashFunc := hash.CustomSHA256Hasher() txGasPerPeriod := bState.TransactionsGasPerPeriod() var nonStakersGasPerPeriod uint64 // Ignore nonStakersGasPerPeriod in post-FastexPhase1 fork. if bState.Version() < version.FastexPhase1 { nonStakersGasPerPeriod = bState.NonStakersGasPerPeriod() } ... ``` In addition, a similar scenario occurs in the sync committee members selection: **beacon-chain/core/altair/sync_committee.go** ```go=61 func NextSyncCommittee(ctx context.Context, s state.BeaconState) (*ethpb.SyncCommittee, error) { var indices []primitives.ValidatorIndex var err error if s.Version() < version.FastexPhase1 { indices, err = NextSyncCommitteeIndices(ctx, s) } else { indices, err = NextSyncCommitteeIndicesFastexPhase1(ctx, s) } ``` Before the `FastexPhase1`, the `NextSyncCommittee()` calls the `NextSyncCommitteeIndices()` that needs the activity score to compute the validator power: **beacon-chain/core/altair/sync_committee.go** ```go=88 func NextSyncCommitteeIndices(ctx context.Context, s state.BeaconState) ([]primitives.ValidatorIndex, error) { epoch := coreTime.NextEpoch(s) indices, err := helpers.ActiveValidatorIndices(ctx, s, epoch) if err != nil { return nil, err } seed, err := helpers.Seed(s, epoch, params.BeaconConfig().DomainSyncCommittee) if err != nil { return nil, err } count := uint64(len(indices)) cfg := params.BeaconConfig() syncCommitteeSize := cfg.SyncCommitteeSize cIndices := make([]primitives.ValidatorIndex, 0, syncCommitteeSize) hashFunc := hash.CustomSHA256Hasher() txGasPerPeriod := s.TransactionsGasPerPeriod() var nonStakersGasPerPeriod uint64 // Ignore nonStakersGasPerPeriod in post-FastexPhase1 forks. if s.Version() < version.FastexPhase1 { nonStakersGasPerPeriod = s.NonStakersGasPerPeriod() } ... ``` In the post `FastexPhase1`, it invokes the `NextSyncCommitteeIndicesFastexPhase1()` that only utilizes the **effective balance**: ```go=174 func NextSyncCommitteeIndicesFastexPhase1(ctx context.Context, s state.BeaconState) ([]primitives.ValidatorIndex, error) { epoch := coreTime.NextEpoch(s) indices, err := helpers.ActiveValidatorIndices(ctx, s, epoch) if err != nil { return nil, err } seed, err := helpers.Seed(s, epoch, params.BeaconConfig().DomainSyncCommittee) if err != nil { return nil, err } count := uint64(len(indices)) cfg := params.BeaconConfig() syncCommitteeSize := cfg.SyncCommitteeSize cIndices := make([]primitives.ValidatorIndex, 0, syncCommitteeSize) hashFunc := hash.CustomSHA256Hasher() for i := primitives.ValidatorIndex(0); uint64(len(cIndices)) < params.BeaconConfig().SyncCommitteeSize; i++ { if ctx.Err() != nil { return nil, ctx.Err() } ... ```
Open the reportFiles: - `beacon-chain/core/altair/sync_committee.go` - `beacon-chain/core/helpers/validators.go` - `config/params/testnet_fastex_chain_config.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The block producer and sync committee member selection inherits the algorithm from the RANDAO randomness generation in the Ethereum Proof of Stake. In Ethereum Proof of Stake, the selection is performed through a shuffle to make the list of active validators randomly, then for each validator, a random number `rand` is generated between 0 and `MaxRand` to check if the inequality $$\frac{s_i}{s} \geq \frac{rand}{MaxRand}$$ holds, where $s_i$ is the effective balance of the validator $i$ and $s$ is the max effective balance. In the Fasttoken, the same approach is adopted with the following modification in the inequality $$(2\cdot \frac{1}{1+e^{-1.5 \cdot \frac{P_i}{P}}} - 1) \cdot \frac{s_i}{s} \geq \frac{rand}{MaxRand} \cdot 0.62$$ The sigmoid function on the left is used to adjust the effective balance of the validator, where the $P_i$ is the voting power of the validator $i$ and $P$ is the max voting power of all the validators. The current value of `SigmoidLimit` is **0.62** on the right, but the maximum value of the sigmoid function $2\cdot \frac{1}{1+e^{-1.5 \cdot \frac{P_i}{P}}} - 1$ is around **0.635** when the $P_i$ equals to $P$. In this case, if the voting power of validator $i$, $P_i = P * 97\%$, then the value of the sigmoid is **0.62**. That means a validator only needs $97\%$ (not $100\%$) of the maximum voting power $P$ to obtain the same formula as the Ethereum Proof of Stake.
Open the reportFiles: - `beacon-chain/core/altair/sync_committee.go` - `beacon-chain/core/helpers/validators.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) According to the Fasttoken consensus whitepaper, the voting power of validator $i$ is defined as: $$ P^e_i = T_e \cdot \frac{s_i}{S} + A^e_{V_i},$$ where $T_e$ is the sum of all transaction constant gas usage components, that is, $T_e = 21000 ∗ N$ where N is the number of all transactions which have been executed during the epoch $e$. However, in line 389 of the implementation: **beacon-chain/core/helpers/validators.go**, **beacon-chain/core/altair/sync_committee.go** ```go=343 func ComputeProposerIndex(bState state.ReadOnlyBeaconState, activeIndices []primitives.ValidatorIndex, seed [32]byte) (primitives.ValidatorIndex, error) { length := uint64(len(activeIndices)) if length == 0 { return 0, errors.New("empty active indices list") } maxRandomByte := new(big.Float).SetUint64(1<<16 - 1) hashFunc := hash.CustomSHA256Hasher() txGasPerPeriod := bState.TransactionsGasPerPeriod() var nonStakersGasPerPeriod uint64 // Ignore nonStakersGasPerPeriod in post-FastexPhase1 fork. if bState.Version() < version.FastexPhase1 { nonStakersGasPerPeriod = bState.NonStakersGasPerPeriod() } totalBalance := TotalBalance(bState, activeIndices) maxPower, err := MaxPower(bState, activeIndices, totalBalance, txGasPerPeriod, nonStakersGasPerPeriod) maxPowerFloat := new(big.Float).SetInt(maxPower) if err != nil { return 0, err } for i := uint64(0); ; i++ { candidateIndex, err := ComputeShuffledIndex(primitives.ValidatorIndex(i%length), length, seed, true /* shuffle */) if err != nil { return 0, err } candidateIndex = activeIndices[candidateIndex] if uint64(candidateIndex) >= uint64(bState.NumValidators()) { return 0, errors.New("active index out of range") } b := append(seed[:], bytesutil.Bytes8(i/16)...) hash := hashFunc(b) bytes2 := append([]byte{}, hash[i%16], hash[16+i%16]) randomBytes := new(big.Float).SetUint64(uint64(bytesutil.FromBytes2(bytes2))) v, err := bState.ValidatorAtIndexReadOnly(candidateIndex) if err != nil { return 0, err } totalBalanceBig := new(big.Int).SetUint64(totalBalance / params.BeaconConfig().EffectiveBalanceIncrement) effectiveBalanceBig := new(big.Int).SetUint64(v.EffectiveBalance() / params.BeaconConfig().EffectiveBalanceIncrement) effectiveActivityBig := new(big.Int).SetUint64(v.EffectiveActivity()) txGasBig := new(big.Int).SetUint64(txGasPerPeriod) nonStakersGasBig := new(big.Int).SetUint64(nonStakersGasPerPeriod) var power *big.Int power = new(big.Int).Add(txGasBig, nonStakersGasBig) power = new(big.Int).Mul(power, effectiveBalanceBig) power = new(big.Int).Div(power, totalBalanceBig) power = new(big.Int).Add(power, effectiveActivityBig) ... ``` $$T_e = T + B$$, where $T$ is the aggregation of the constant gas usage argument equal to 21000, and $B$ is the gas usage of smart contracts not associated with any validator, both of which are calculated in the sliding window of 1575 epochs, not in each epoch. The $B$ part, `nonStakersGasBig` is zero only in the post-FastexPhase1 fork (shown in lines 354-356), which matches the formula in the whitepaper.
Open the reportFiles: - `config/params/mainnet_config.go` - `config/params/minimal_config.go` Commit: - [`3b8da2895d7067405b54c0829eee7e044a0f978e`](https://github.com/fastexlabs/bahamut-consensus/tree/3b8da2895d7067405b54c0829eee7e044a0f978e) The parameters `MaxEffectiveBalance` and `EjectionBalance` were properly set in the configuration file `testnet_fastex_chain_config.go`. However, they were not updated in the configuration file `mannet_config.go` to accommodate the new features and functionality. A misconfiguration could cause errors or bugs that could negatively impact the functionality of the project. ```go=92 MaxEffectiveBalance: 32 * 1e9, EjectionBalance: 16 * 1e9, ``` In addition, the below parameters in the configuration file `minimal_config.go` are not properly set. ```go=20 MinGenesisTime: 1606824000, // Dec 1, 2020, 12pm UTC. ``` ```go=25 minimalConfig.MinDepositAmount = 1e9 minimalConfig.MaxEffectiveBalance = 32e9 minimalConfig.EjectionBalance = 16e9 minimalConfig.EffectiveBalanceIncrement = 1e9 ```
Open the reportFiles: - `beacon-chain/core/fastex-phase1/attestation.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) The function `RewardProposer()` is intended to calculate the reward for the block proposer, which accepts the parameters, `proposerRewardNumerator` and `proposerRewardDenominator` from the return values of function `EpochParticipation()`. If the passed `indices` is empty in the loop of the `EpochParticipation()`, both `proposerRewardNumerator` and `proposerRewardDenominator` will be 0 and the returned error is nil. ```go=129 func EpochParticipation( beaconState state.BeaconState, indices []uint64, epochParticipation []byte, participatedFlags map[uint8]bool, totalBalance uint64, ) (uint64, uint64, []byte, error) { cfg := params.BeaconConfig() sourceFlagIndex := cfg.TimelySourceFlagIndex targetFlagIndex := cfg.TimelyTargetFlagIndex headFlagIndex := cfg.TimelyHeadFlagIndex proposerRewardNumerator := uint64(0) proposerRewardDenominator := uint64(0) for _, index := range indices { ... } return proposerRewardNumerator, proposerRewardDenominator, epochParticipation, nil } ``` In this case, error handling in lines 99-101 and 109-110 of the function `SetParticipationAndRewardProposer()` will be bypassed. ```go=84 func SetParticipationAndRewardProposer( ctx context.Context, beaconState state.BeaconState, targetEpoch primitives.Epoch, indices []uint64, participatedFlags map[uint8]bool, totalBalance uint64, ) (state.BeaconState, error) { var proposerRewardNumerator uint64 var proposerRewardDenominator uint64 currentEpoch := time.CurrentEpoch(beaconState) var stateErr error if targetEpoch == currentEpoch { stateErr = beaconState.ModifyCurrentParticipationBits(func(val []byte) ([]byte, error) { propRewardNum, propRewardDenom, epochParticipation, err := EpochParticipation(beaconState, indices, val, participatedFlags, totalBalance) if err != nil { return nil, err } proposerRewardNumerator = propRewardNum proposerRewardDenominator = propRewardDenom return epochParticipation, nil }) } else { stateErr = beaconState.ModifyPreviousParticipationBits(func(val []byte) ([]byte, error) { propRewardNum, propRewardDenom, epochParticipation, err := EpochParticipation(beaconState, indices, val, participatedFlags, totalBalance) if err != nil { return nil, err } proposerRewardNumerator = propRewardNum proposerRewardDenominator = propRewardDenom return epochParticipation, nil }) } if stateErr != nil { return nil, stateErr } if err := RewardProposer(ctx, beaconState, proposerRewardNumerator, proposerRewardDenominator); err != nil { return nil, err } return beaconState, nil } ``` In addition, the error handling in lines 117-119 will also be bypassed, allowing the function `RewardProposer()` with `proposerRewardDenominator` as 0 to be invoked in the function `SetParticipationAndRewardProposer()`. Therefore, the parameter `proposerRewardDenominator` passed in the `RewardProposer()` is 0.
Open the reportFiles: - `beacon-chain/core/blocks/activities.go` Commit: - [`3b8da2895d7067405b54c0829eee7e044a0f978e`](https://github.com/fastexlabs/bahamut-consensus/tree/3b8da2895d7067405b54c0829eee7e044a0f978e) The function `ProcessBlockActivities()` is intended to process the activities in a block, which invokes the function `ProcessActivity()` with each `activity` from the block. Since the activities are fetched from the execution layer, they could possibly contain some nil value. If the `activity` is nil, then fetching `ContractAddress` from the `activity` in line 42 of the function `ProcessActivity()` will lead to a runtime panic. ```go=34 func ProcessActivity( ctx context.Context, beaconState state.BeaconState, activity *ethpb.ActivityChange, ) (state.BeaconState, error) { ctx, span := trace.StartSpan(ctx, "core.ProcessActivtiyNoVerifySignature") defer span.End() contract := bytesutil.ToBytes20(activity.ContractAddress) idx, ok := beaconState.ValidatorIndexByContractAddress(contract) if !ok { nonStakersGas := beaconState.NonStakersGasPerEpoch() if err := beaconState.SetNonStakersGasPerEpoch(nonStakersGas + activity.DeltaActivity); err != nil { return nil, err } return beaconState, nil } ... ```
Open the reportFiles: - `beacon-chain/core/altair/reward.go` Commit: - [`3226f8330911cb8df77e775f0155b335ba771bd8`](https://github.com/fastexlabs/bahamut-consensus/tree/3226f8330911cb8df77e775f0155b335ba771bd8) - [`8198a02d28dee2b7485610279bcf24e4f0a2bf54`](https://github.com/fastexlabs/bahamut-consensus/commit/8198a02d28dee2b7485610279bcf24e4f0a2bf54) The fasttoken introduces a novel proposer base reward calculation based on the validator's power via the activity score associated with the validator's registered contracts. The function `BaseProposerReward()` is used to compute the base proposer reward defined in the whitepaper as follows: $$BPR = \frac{(A+T)*bf}{W * n * gwei},$$ where $A = \sum_{i=1}^{n} ea_i$ is the total effective activities of the $n$ validators and $T$ is the transaction constant gas in the window of 1575 epochs. The $bf$ is the base fee of the block and $W$ is the epoch window size 1575, $n$ is the number of validators and $gwei$ is the constant $10^9$. Therefore, the base proposer reward in each epoch is $BPR * s$, where $s$ (= 32) is the number of slots in an epoch. On the other hand, the total validator base reward per epoch is given by: $$BR_{total} = \frac{B*f}{\sqrt{B}} = f*\sqrt{B},$$ where $B$ is the total active balance, and $f$ is the constant 156 (according to the `config/params/mainnet_config.go` in the commit [`8198a02d28dee2b7485610279bcf24e4f0a2bf54`](https://github.com/fastexlabs/bahamut-consensus/commit/8198a02d28dee2b7485610279bcf24e4f0a2bf54)). Assume that the current number of validators is the target number 4096, and each of them has an effective balance of $8192 * 10^9$. Then the base reward is $$156 * \sqrt{4096 * 8192 * 10^9} = 0.9 * 10^9 gwei$$. According to the design, this reward will be distributed to the validators for attestation rewards and participating sync committees. In the Ethereum PoS, $1/7$ of the reward (i.e., $0.13 * 10^9$) is granted to the block proposers for proposing blocks. The fasttoken attempts to use the base proposer reward in each epoch (i.e., $32*BPR$) to replace the $1/7$ of the total validator base reward per epoch as the reward to the block proposers. In this case, assume that each block has a half load (15M gas consumed) and the base fee is $100 * gwei$, then the base proposer reward in each epoch is $$32 * \frac{(A+T)*bf}{W * n * gwei} = 32 * \frac{(32 *15M)* 100 * gwei}{4096 * gwei} = 0.395 * 10^9 gwei,$$ Therefore, the calculated reward is close to the value in the new design. However, in the implementation of the function `BaseProposerReward()`: ```go=65 func BaseProposerReward(s state.ReadOnlyBeaconState, totalPower, totalEffectivePower uint64) (uint64, error) { activity, err := helpers.TotalEffectiveActivity(s) if err != nil { return 0, errors.Wrap(err, "could not calculate total effective activity") } sharedActivity := s.SharedActivity() if sharedActivity == nil { return 0, errors.New("nil shared activity in state") } period := uint64(params.BeaconConfig().EpochsPerActivityPeriod) slotsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch) denominator := period * period * slotsPerEpoch * slotsPerEpoch transactionsGas := sharedActivity.TransactionsGasPerPeriod baseFee := sharedActivity.BaseFeePerPeriod reward := baseFee * (activity + transactionsGas) / denominator if totalPower == 0 { return reward, nil } return reward * totalEffectivePower / totalPower, nil } ``` The reward does not align with the formula in the whitepaper as the reward is not divided by the number of active validators. Actually, the formula derived from the above code on average is as follows: $$ (A+T)*bf = 15 M * 100 gwei = 1.5 * 10^9 gwei$$.
Open the reportFiles: - `core/state/state_object.go` - `core/state_transition.go` Commit: - [`af75d5f6c6ab5a33f6a1ac86c5c443e7be943cf1`](https://github.com/fastexlabs/bahamut-execution/tree/af75d5f6c6ab5a33f6a1ac86c5c443e7be943cf1) There are no overflow and underflow protections in the following functions, making it possible for overflow/underflow to occur and could possibly lead to inaccurate calculations. **core/state/state_object.go** ```go=400 func (s *stateObject) AddActivity(amount uint64) { if amount == 0 { if s.empty() { s.touch() } return } s.SetActivity(s.Activity() + amount) } // SubActivity remove some amount of activity to s's activity func (s *stateObject) SubActivity(amount uint64) { if amount == 0 { if s.empty() { s.touch() } return } s.SetActivity(s.Activity() - amount) } ``` **core/state_transition.go** ```go=396 func (st *StateTransition) refundActivity(refund uint64) { if refund == 0 { return } totalRefund := refund totalActivityByContract := make(map[common.Address]uint64) totalRefundsByContracts := make(map[common.Address]uint64) currentActivities := st.state.GetCurrentActivities() for _, act := range currentActivities { totalActivityByContract[act.Address] += act.DeltaActivity } var proportion []float64 for _, act := range currentActivities { proportion = append(proportion, float64(act.DeltaActivity)/float64(totalActivityByContract[act.Address])) } for i, act := range currentActivities { if i == len(currentActivities)-1 { totalRefundsByContracts[act.Address] += refund act.DeltaActivity -= refund act.Activity -= totalRefundsByContracts[act.Address] st.state.SubActivity(act.Address, refund) log.Debug("Refunded contract activity", "activity", refund, "addr", act.Address) } else { totalRefundByContract := float64(totalRefund*st.state.GetRefundsByContract(act.Address)) / float64(st.state.GetRefund()) refundAct := uint64(totalRefundByContract * proportion[i]) totalRefundsByContracts[act.Address] += refundAct act.DeltaActivity -= refundAct act.Activity -= totalRefundsByContracts[act.Address] st.state.SubActivity(act.Address, refundAct) refund -= refundAct log.Debug("Refunded contract activity", "activity", totalRefundByContract, "addr", act.Address) } } } ```
Open the reportFiles: - `beacon-chain/rpc/prysm/v1alpha1/validator/proposer_bellatrix.go` Commit: - [`3226f8330911cb8df77e775f0155b335ba771bd8`](https://github.com/fastexlabs/bahamut-consensus/tree/3226f8330911cb8df77e775f0155b335ba771bd8) The function `unblindBuilderBlock()` retrieves the full payload block using the input blind block. However, the output block does not contain the fields `ActivityChanges`, `TransactionsCount`, `BaseFee`, And `ExecutionHeight`. ```go=306 Body: ðpb.BeaconBlockBodyBellatrix{ RandaoReveal: psb.Block.Body.RandaoReveal, Eth1Data: psb.Block.Body.Eth1Data, Graffiti: psb.Block.Body.Graffiti, ProposerSlashings: psb.Block.Body.ProposerSlashings, AttesterSlashings: psb.Block.Body.AttesterSlashings, Attestations: psb.Block.Body.Attestations, Deposits: psb.Block.Body.Deposits, VoluntaryExits: psb.Block.Body.VoluntaryExits, SyncAggregate: agg, ExecutionPayload: pbPayload, }, ```
Open the reportFiles: - `beacon-chain/core/blocks/activities.go` - `beacon-chain/core/fastex-phase1/attestation.go` - `beacon-chain/p2p/pubsub_filter.go` - `beacon-chain/rpc/apimiddleware/custom_hooks.go` - `beacon-chain/node/node.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) There are some typos in the error messages in the current codebase: **beacon-chain/core/blocks/activities.go** - in line 26, `activties` should be `activities`; - in line 39, `core.ProcessActivtiyNoVerifySignature` should be `core.ProcessActivityNoVerifySignature`. **beacon-chain/core/fastex-phase1/attestation.go** - in line 55, `altair.ProcessAttestationNoVerifySignature` should be `fastexphase1.ProcessAttestationNoVerifySignature`; **beacon-chain/p2p/pubsub_filter.go** - in line 57, `Could not determine Bellatrix fork digest` should be `Could not determine fastexPhase1 fork digest`. **beacon-chain/rpc/apimiddleware/custom_hooks.go** - in line 849, `4 unsupported block version '%s'` should be `unsupported block version '%s'`. **beacon-chain/node/node.go** - in line 229, `Registering Determinstic Genesis Service` should be `Registering Deterministic Genesis Service`; - in line 239, `Registering Intial Sync Service` should be `Registering Initial Sync Service`.
Open the reportFiles: - `beacon-chain/core/helpers/validators.go` Commit: - [`3226f8330911cb8df77e775f0155b335ba771bd8`](https://github.com/fastexlabs/bahamut-consensus/tree/3226f8330911cb8df77e775f0155b335ba771bd8) In the line 498, 504 and 508 of file `beacon-chain/core/helpers/validators.go`, the function name `isEligibileForActivationQueue()` is supposed to be `isEligibleForActivationQueue()`.
Open the reportFiles: - `beacon-chain/state/stateutil/activity_changes_root.go` - `beacon-chain/state/stateutil/field_root_shared_activity.go` - `beacon-chain/state/stateutil/shared_activity_root.go` Commit: - [`cffbd04e743737989e44cf0ebae70fd353c5a539`](https://github.com/fastexlabs/bahamut-consensus/commit/cffbd04e743737989e44cf0ebae70fd353c5a539) **beacon-chain/state/stateutil/activity_changes_root.go** - in lines 17 and 23, `merkleiztion` should be `merkleization`. **beacon-chain/state/stateutil/field_root_shared_activity.go** - in line 15, function name `SharedActivityRootWithHaher()` should be `SharedActivityRootWithHasher()`. **beacon-chain/state/stateutil/shared_activity_root.go** - in line 13, function name `SharedActivityRootWithHaher()` should be `SharedActivityRootWithHasher()`.
Open the reportFiles: - `core/vm/stateful_contracts.go` Commit: - [`716ea69939139eab9f45b4c68347eb67de492bea`](https://github.com/fastexlabs/bahamut-execution/tree/716ea69939139eab9f45b4c68347eb67de492bea) In the aforementioned places, `statefulPrecomiledContractWithSelectors` should be `statefulPrecompiledContractWithSelectors`.
Open the reportFiles: - `config/params/config.go` - `config/params/mainnet_config.go` - `config/features/config.go` - `config/features/flags.go` - `beacon-chain/rpc/prysm/v1alpha1/validator/proposer_eth1data.go` - `validator/keymanager/remote-web3signer/v1/custom_mappers.go` - `validator/keymanager/remote-web3signer/v1/web3signer_types.go` - `beacon-chain/execution/activities_processing.go` - `beacon-chain/rpc/apimiddleware/structs.go` - `beacon-chain/node/node.go` Commit: - [`33b75d4e162179d360e60ac88bb4289293b530a6`](https://github.com/fastexlabs/bahamut-consensus/tree/33b75d4e162179d360e60ac88bb4289293b530a6) **Variable Names** According to the context, the variable `EpochsPerAcrivityUpdate` should be **`EpochsPerActivityUpdate`** in the following two places: 1. line 145 in the file `config/params/config.go`; 2. line 202 in the file `config/params/mainnet_config.go`. The variable `DisableStakinContractCheck` (`disableStakinContractCheck`) should be **`DisableStakingContractCheck`** (**`disableStakingContractCheck`**) in the following places: 1. line 73, 209, 210, and 211 in the file `config/features/config.go`; 2. line 85 and 176 in the file `config/features/flags.go`; 3. line 119 in the file `beacon-chain/rpc/prysm/v1alpha1/validator/proposer_eth1data.go`. The variable `ContractAddres` should be **`ContractAddress`** in the following places: 1. line 288 in the file `validator/keymanager/remote-web3signer/v1/custom_mappers.go`; 2. line 183 in the file `validator/keymanager/remote-web3signer/v1/web3signer_types.go`. The variable `activiyChanges` should be **`activityChanges`** in the following place: - line 62 in the file `beacon-chain/execution/activities_processing.go`. The variable `EffectivtActivity` should be **`EffectiveActivity`** in the following place: - line 1019 in the file `beacon-chain/rpc/apimiddleware/structs.go`. ---- **Function Names** The function name `registerDeterminsticGenesisService()` should be **registerDeterministicGenesisService()** in the following places: - line 230 and 920 in the file `beacon-chain/node/node.go`.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportIt is not recommended to use Solidity's `transfer()` and `send()` functions for transferring Ether, since some contracts may not be able to receive the funds. Those functions forward only a fixed amount of gas (2300 specifically) and the receiving contracts may run out of gas before finishing the transfer. Also, EVM instructions' gas costs may increase in the future. Thus, some contracts that can receive now may stop working in the future due to the gas limitation. ```solidity=178 recipientAddress_.transfer(address(this).balance); ``` - `FasttokenDistribution.refund` uses `transfer()`.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="bahamut"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us