Olas - cheatc0d3's results

Olas is a unified network for off-chain services like automation, oracles, co-owned AI. It offers a stack for building services and a protocol for incentivizing their creation and their operation in a co-owned and decentralized way.

General Information

Platform: Code4rena

Start Date: 21/12/2023

Pot Size: $90,500 USDC

Total HM: 10

Participants: 39

Period: 18 days

Judge: LSDan

Total Solo HM: 5

Id: 315

League: ETH

Olas

Findings Distribution

Researcher Performance

Rank: 29/39

Findings: 1

Award: $21.90

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

21.8971 USDC - $21.90

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
Q-01

External Links

Validate Proposal Parameters:

The propose function does not validate the input parameters, which could lead to invalid proposals being created.

Lines of code:

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L45C5-L53C6

function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) public override returns (uint256) { require(targets.length == values.length, "Mismatched targets and values"); require(targets.length == calldatas.length, "Mismatched targets and calldatas"); require(targets.length > 0, "Must provide at least one target"); return super.propose(targets, values, calldatas, description); }

Validating that the lengths of targets, values, and calldatas arrays match ensures that each target has a corresponding value and calldata. Checking that there is at least one target prevents empty proposals.

Enforce Proposal Threshold:

The contract may allow proposals with insufficient support to be created if the proposal threshold is not enforced.

Lines of Code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L45C5-L53C6

function propose( // ... parameters ) public override returns (uint256) { require(getVotes(msg.sender, block.number - 1) >= proposalThreshold(), "Proposer votes below threshold"); // Existing logic... }

Checking that the proposer's votes meet the proposal threshold ensures that only those with sufficient support can create proposals, which is a key governance safeguard.

Execution and Cancellation Authorization:

The _execute and _cancel functions are internal and assume that the caller is authorized, but additional checks could be added for extra security.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L68C5-L77C6

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L85C5-L93C6

function _execute( // parameters ) internal override { require(state(proposalId) == ProposalState.Queued, "Proposal must be queued"); } function _cancel( ) internal override { require(state(proposalId) == ProposalState.Pending, "Proposal must be pending"); }

Adding state checks ensures that proposals are in the correct state before execution or cancellation, preventing unauthorized or premature actions.

Denial of Service on Proposals:

The propose function allows a user to submit a new proposal. If there are no checks on the frequency or number of proposals a user can make, this could lead to a DoS attack by spamming the governance system with proposals.

Lines of code:

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L45C5-L53C6

// @audit Add a mapping to track the last proposal timestamp for each address mapping(address => uint256) public lastProposalTimestamp; function propose( ) public override returns (uint256) { require(block.timestamp - lastProposalTimestamp[msg.sender] >= proposalCooldown, "Must wait for cooldown period"); lastProposalTimestamp[msg.sender] = block.timestamp; } uint256 public proposalCooldown = 1 weeks; // @audit Cooldown period between proposals from the same address

Introducing a cooldown period between proposals from the same address can prevent spam and ensure that governance resources are used effectively.

Add Execution Delay for Proposals:

There is no enforced delay between when a proposal is created and when it can be executed, which could lead to rushed decisions.

Lines of code:

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L45C5-L53C6

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L68C5-L77C6

function _execute( // parameters ) internal override { require(block.timestamp >= proposalTimelock[proposalId], "Execution before timelock"); } mapping(uint256 => uint256) public proposalTimelock; function propose( // parameters ) public override returns (uint256) { uint256 proposalId = super.propose(targets, values, calldatas, description); proposalTimelock[proposalId] = block.timestamp + executionDelay; return proposalId; } uint256 public executionDelay = 2 days; // @audit Delay before a proposal can be executed

Adding an execution delay ensures that there is a period for reflection and discussion before a proposal is executed, promoting thoughtful governance.

Validate Proposal Cancellation Parameters:

Ensure that the parameters provided to the _cancel function match the proposal details to prevent cancellation with incorrect data.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L68C5-L77C6

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L85C5-L93C6

function _cancel( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override returns (uint256) { require(targets.length == values.length, "Mismatch between targets and values"); require(targets.length == calldatas.length, "Mismatch between targets and calldatas"); }

Validating the length of the arrays ensures that each target has a corresponding value and calldata, same as _execute function.

Short-Circuiting Conditions:

Evaluating conditions that are unlikely to be true can waste gas.

Lines of code:

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/GovernorOLAS.sol#L68

function _execute( uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override { ProposalState currentState = state(proposalId); if (currentState != ProposalState.Queued) { return; // Short-circuit to save gas if the proposal is not in the queued state } super._execute(proposalId, targets, values, calldatas, descriptionHash); }

Using short-circuiting in conditionals can save gas by avoiding unnecessary computation when the condition is likely to be false.

Change Owner and Minter with Multi-Signature Control:

A single account can change the owner or minter, which is a centralization risk.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/OLAS.sol#L43C5-L54C6

// @audit Introduce a multi-signature requirement for changing critical roles function changeOwner(address newOwner) external onlyMultisig { require(newOwner != address(0), "ZeroAddress"); owner = newOwner; emit OwnerUpdated(newOwner); } function changeMinter(address newMinter) external onlyMultisig { require(newMinter != address(0), "ZeroAddress"); minter = newMinter; emit MinterUpdated(newMinter); } // @audit Modifier to check if the caller is a multisig contract modifier onlyMultisig() { require(isMultisig(msg.sender), "Not multisig"); _; }

This change introduces a multi-signature requirement for changing the owner or minter, reducing the risk of a single point of failure.

Block Timestamp Manipulation Protection:

The use of block.timestamp can be manipulated by miners to a certain degree, which might affect the inflation control mechanism.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/OLAS.sol#L98C5-L111C10

function inflationRemainder() public view returns (uint256 remainder) { uint256 _totalSupply = totalSupply; // Current year uint256 numYears = (block.timestamp - timeLaunch) / oneYear; // Calculate maximum mint amount to date uint256 supplyCap = tenYearSupplyCap; // After 10 years, adjust supplyCap according to the yearly inflation % set in maxMintCapFraction if (numYears > 9) { // Number of years after ten years have passed (including ongoing ones) numYears -= 9; for (uint256 i = 0; i < numYears; ++i) { supplyCap += (supplyCap * maxMintCapFraction) / 100; } }

Use a more reliable measure of time passage, such as block numbers, or introduce a tolerance for timestamp manipulation to mitigate the impact of any potential manipulation.

Inflation Control Precision:

To address potential precision errors in the inflationControl function, we use SafeMath for arithmetic operations.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/OLAS.sol#L98C5-L111C10

import "@openzeppelin/contracts/utils/math/SafeMath.sol"; function inflationRemainder() public view returns (uint256 remainder) { uint256 _totalSupply = totalSupply; uint256 numYears = SafeMath.sub(block.timestamp, timeLaunch) / oneYear; uint256 supplyCap = tenYearSupplyCap; if (numYears > 9) { uint256 additionalYears = SafeMath.sub(numYears, 9); for (uint256 i = 0; i < additionalYears; ++i) { supplyCap = SafeMath.add(supplyCap, SafeMath.div(SafeMath.mul(supplyCap, maxMintCapFraction), 100)); } } remainder = SafeMath.sub(supplyCap, _totalSupply); }

Using SafeMath's add, sub, mul, and div functions ensures that arithmetic operations in the inflationRemainder function are performed safely without overflow or underflow, maintaining precision.

Centralization of Power in Owner and Minter Roles:

The changeOwner and changeMinter functions allow the owner to unilaterally change critical roles in the contract.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/OLAS.sol#L43C5-L54C6

function changeOwner(address newOwner) external { if (msg.sender != owner) { revert ManagerOnly(msg.sender, owner); } // ... }

Implement a multi-signature mechanism or a time-delayed commit-reveal scheme to require multiple confirmations before such critical changes take effect.

Inefficient Loop in Inflation Calculation:

The loop in inflationRemainder can lead to high gas costs and could be exploited to cause a denial-of-service attack by repeatedly calling the function with parameters that maximize loop iterations.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/OLAS.sol#L108

for (uint256 i = 0; i < numYears; ++i) { supplyCap += (supplyCap * maxMintCapFraction) / 100; }

Replace the loop with a closed-form formula for compound interest calculation to avoid iteration and reduce gas costs.

Rounding Errors in Inflation Calculation:

The use of integer division in the inflation calculation can lead to rounding errors, potentially causing discrepancies in the supply cap.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/OLAS.sol#L109

supplyCap += (supplyCap * maxMintCapFraction) / 100;

Use a fixed-point arithmetic library or implement a rounding policy to minimize the impact of rounding errors.

Optimize Storage Access in Loops

Accessing storage variables in loops can be gas-intensive.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/veOLAS.sol#L690

function _supplyLockedAt(PointVoting memory lastPoint, uint64 ts) internal view returns (uint256 vSupply) { // @audit Cache storage variables in memory before the loop int128 slope = lastPoint.slope; int128 bias = lastPoint.bias; uint64 lastTs = lastPoint.ts; // @audit Use cached variables in the loop }

Caching storage variables in memory before a loop and using the cached variables within the loop can save gas.

Use SafeMath for Arithmetic Operations

The contract performs arithmetic operations without explicitly checking for overflows or underflows.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/veOLAS.sol#L330C5-L343C10

import "@openzeppelin/contracts/utils/math/SafeMath.sol"; function _depositFor( // parameters ) internal { // @audit Use SafeMath for arithmetic operations supply = SafeMath.add(supply, amount); }

Using SafeMath for arithmetic operations ensures that they are performed safely without overflow or underflow, which is especially important for financial contracts.

Validate Unlock Time in Increase Unlock Time Function

The increaseUnlockTime function does not validate that the new unlock time is greater than the current unlock time.

function increaseUnlockTime(uint256 unlockTime) external { LockedBalance storage lockedBalance = mapLockedBalances[msg.sender]; uint256 newUnlockTime = ((block.timestamp + unlockTime) / WEEK) * WEEK; require(newUnlockTime > lockedBalance.endTime, "New unlock time must be greater than current unlock time"); // Existing logic... }

This validation ensures that users can only increase their lock time, not decrease it, which is important for maintaining the integrity of the voting power system.

Validate Lock Extension in Increase Unlock Time Function:

Ensure that the new unlock time is in the future and extends the current lock.

function increaseUnlockTime(uint256 unlockTime) external { LockedBalance storage userLock = mapLockedBalances[msg.sender]; require(unlockTime > userLock.endTime, "New unlock time must be greater than current unlock time"); // Existing logic... }

This validation ensures that users can only extend their lock time, not decrease it, which is important for maintaining the integrity of the voting power system.

Check Effects Interactions Pattern: The contract should follow the checks-effects-interactions pattern to prevent reentrancy issues. This means that state changes should occur before external calls. Before: IERC20(token).transfer(msg.sender, amount); mapLockedBalances[msg.sender] = LockedBalance(0, 0); Recommended: mapLockedBalances[msg.sender] = LockedBalance(0, 0); IERC20(token).transfer(msg.sender, amount);

Updating the state before making the external call ensures that the contract's state is consistent even if the external call leads to a reentrant call back into the contract.

Use of transferFrom and transfer without checking return value:

The contract assumes that the transferFrom and transfer functions of the ERC20 token will revert on failure. While this is a common pattern, especially with tokens that follow the ERC20 standard closely, it is not guaranteed by the standard itself.

IERC20(token).transferFrom(msg.sender, address(this), amount); ... IERC20(token).transfer(msg.sender, amount);
bool sent = IERC20(token).transferFrom(msg.sender, address(this), amount); require(sent, "Token transfer failed"); ... sent = IERC20(token).transfer(msg.sender, amount); require(sent, "Token transfer failed");

Checking the return value of the transferFrom and transfer calls and requiring that it is true ensures that the token transfer was successful. This is a safer approach and can prevent subtle bugs if the ERC20 token does not revert on failure.

Use of Block Timestamps for Critical Logic

The contract uses block timestamps for calculating lock periods and voting weights. Block timestamps can be slightly manipulated by miners and are not precise to the second.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/veOLAS.sol#L192

if (newLocked.endTime > block.timestamp && newLocked.amount > 0) { uNew.slope = int128(newLocked.amount) / IMAXTIME; uNew.bias = uNew.slope * int128(uint128(newLocked.endTime - uint64(block.timestamp))); }

Reliance on block timestamps for critical logic can lead to inaccuracies in voting power calculations. In extreme cases, miner manipulation of timestamps could be used to gain an advantage in governance decisions.

Gas Limit DoS with Block Stuffing

The _checkpoint function iterates over a potentially large number of weeks to fill in the history. If the number of iterations is large enough, it could hit the block gas limit, making it impossible to call successfully.

for (uint256 i = 0; i < 255; ++i) { // loop body that potentially runs 255 times }

An attacker could deliberately create conditions that maximize the loop's iterations, causing transactions that call _checkpoint to consistently fail due to out-of-gas errors. This would effectively lock the contract, preventing users from creating, extending, or withdrawing from locks, and could disrupt the governance process.

Lack of Event Emission After Sensitive Changes

The contract does not emit events for certain state changes, such as when user balances are updated or when slope changes are recorded. This lack of transparency can make it difficult to track changes and could be exploited to hide malicious activities.

mapLockedBalances[account] = lockedBalance;

Without events logging these changes, it becomes harder for off-chain services and users to verify and react to changes in the contract state. This could be exploited by an attacker to make stealthy changes that are not easily noticed, potentially leading to unnoticed security breaches or manipulation of the voting power.

Precision Loss in Vote Weight Calculations

The contract calculates vote weight based on the amount of tokens locked and the time remaining until the lock expires. The use of integer division can lead to precision loss, especially with large numbers or small differences in timestamps.

uNew.slope = int128(newLocked.amount) / IMAXTIME;

Precision loss in vote weight calculations could lead to discrepancies in the distribution of voting power. This could be exploited by an attacker to gain a disproportionate amount of influence in governance decisions.

Unchecked Casts

The contract uses unchecked type casts, which could lead to unexpected behavior if the casted value does not fit into the target type.

lockedBalance.endTime = uint64(unlockTime);

If unlockTime is larger than what can be represented by uint64, the cast will truncate the value, potentially leading to incorrect lock end times. This could be exploited to create locks that expire prematurely or never expire, affecting the integrity of the voting system.

Inadequate Handling of ERC20 Decimals

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/veOLAS.sol

The contract assumes that the ERC20 token has 18 decimals, which is standard for many tokens but not guaranteed. This could lead to incorrect calculations of voting power if the token has a different number of decimals.

If the contract interacts with a token that does not have 18 decimals, all calculations related to token amounts and voting power could be incorrect, leading to potential loss of funds or unfair distribution of voting power.

Checking Return Value of ERC20 Transfers

The contract assumes that the transferFrom and transfer functions of the ERC20 token will revert on failure. This assumption may not hold for all ERC20 tokens.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/veOLAS.sol#L364

IERC20(token).transferFrom(msg.sender, address(this), amount); ... IERC20(token).transfer(msg.sender, amount);

Checking the return value of the transferFrom and transfer calls and requiring that it is true ensures that the token transfer was successful. This is a safer approach and can prevent issues if the ERC20 token does not revert on failure.

bool sent = IERC20(token).transferFrom(msg.sender, address(this), amount); require(sent, "Token transfer failed"); ... sent = IERC20(token).transfer(msg.sender, amount); require(sent, "Token transfer failed");

Gas Optimizations for Storage Access

The contract uses storage variables extensively, which can lead to high gas costs.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/veOLAS.sol#L355

mapLockedBalances[account] = lockedBalance;

Using a storage pointer to update multiple fields of a struct in storage can save gas by avoiding multiple SSTORE operations.

LockedBalance storage locked = mapLockedBalances[account]; locked.amount = newAmount; locked.endTime = newEndTime;

Validate Timestamps in Voting Power Calculations:

Ensure that the timestamp used in voting power calculations is not smaller than the last supply point timestamp to prevent incorrect calculations.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L263C5-L273C6

function totalSupplyLockedAtT(uint256 ts) external view returns (uint256 vPower) { uint256 numPoints = IVEOLAS(ve).totalNumPoints(); PointVoting memory sPoint = IVEOLAS(ve).mapSupplyPoints(numPoints - 1); require(ts >= sPoint.ts, "Timestamp is too early"); vPower = IVEOLAS(ve).totalSupplyLockedAtT(ts); }

Adding a check for the timestamp ensures that the voting power calculation uses a valid timestamp, preventing potential errors due to incorrect data.

Check for Valid Supply Point Index:

Ensure that the index provided for supply point retrieval is within bounds to prevent out-of-range errors.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L163C5-L165C6

function mapSupplyPoints(uint256 idx) external view returns (PointVoting memory sPoint) { uint256 numPoints = IVEOLAS(ve).totalNumPoints(); require(idx < numPoints, "Index out of bounds"); sPoint = IVEOLAS(ve).mapSupplyPoints(idx); }

This check ensures that the index is within the range of existing supply points, preventing potential errors due to out-of-range access.

Ensure Block Number is in the Past for Voting Power Calculations

When calculating voting power at a specific block number, ensure that the block number provided is not in the future.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L211C5-L218C6

function getPastVotes(address account, uint256 blockNumber) external view returns (uint256 balance) { require(blockNumber <= block.number, "Future block number provided"); balance = IVEOLAS(ve).getPastVotes(account, blockNumber); }

This check ensures that the function cannot be called with a block number that has not yet occurred, which would otherwise lead to incorrect or undefined behavior.

Validate Slope Changes Timestamp

Ensure that the timestamp for slope changes is valid and does not exceed the current block timestamp.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L170C5-L172C6

function mapSlopeChanges(uint64 ts) external view returns (int128 slopeChange) { require(ts <= uint64(block.timestamp), "Future timestamp provided"); slopeChange = IVEOLAS(ve).mapSlopeChanges(ts); }

This validation ensures that the function cannot be called with a timestamp that is in the future, which would otherwise lead to incorrect or undefined behavior.

Check User Point Index is Within Range:

When retrieving a user point, ensure that the index is within the range of the user's points to prevent out-of-bounds access.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L193C5-L199C6

function getUserPoint(address account, uint256 idx) external view returns (PointVoting memory uPoint) { uint256 userNumPoints = IVEOLAS(ve).getNumUserPoints(account); require(idx < userNumPoints, "Index out of bounds"); uPoint = IVEOLAS(ve).getUserPoint(account, idx); }

This check ensures that the index is within the range of the user's points, preventing potential errors due to out-of-range access.

Validate Lock End Time is in the Future:

When locking tokens, ensure that the lock end time is in the future to prevent locking tokens without any voting power benefit.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L243C5-L245C6

function lockedEnd(address account) external view returns (uint256 unlockTime) { unlockTime = IVEOLAS(ve).lockedEnd(account); require(unlockTime > block.timestamp, "Lock end time is not in the future"); }

This validation ensures that the lock end time is in the future, which is necessary for the tokens to have voting power and for the lock to have a purpose.

Gas Optimization in getUserPoint Function

The getUserPoint function retrieves the number of user points to check if the user has any points before attempting to get a specific point. This can be optimized by directly attempting to fetch the user point and relying on the underlying veOLAS contract to revert if the index is out of bounds.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L193C5-L199C6

function getUserPoint(address account, uint256 idx) public view returns (PointVoting memory uPoint) { uint256 userNumPoints = IVEOLAS(ve).getNumUserPoints(account); if (userNumPoints > 0) { uPoint = IVEOLAS(ve).getUserPoint(account, idx); } }

change to:

function getUserPoint(address account, uint256 idx) public view returns (PointVoting memory uPoint) { uPoint = IVEOLAS(ve).getUserPoint(account, idx); }

The getNumUserPoints call is unnecessary because the underlying veOLAS contract will revert if the index is out of bounds. This saves gas by eliminating an extra external call when the index is valid.

totalSupplyLockedAtT function can be optimized

Adding a check for the existence of supply points and using numPoints - 1 to access the last supply point ensures that we do not attempt to access an index that does not exist. This also prevents a potential underflow if numPoints were to be zero.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/wveOLAS.sol#L263C1-L273C6

function totalSupplyLockedAtT(uint256 ts) external view returns (uint256 vPower) { uint256 numPoints = IVEOLAS(ve).totalNumPoints(); PointVoting memory sPoint = IVEOLAS(ve).mapSupplyPoints(numPoints); if (ts >= sPoint.ts) { vPower = IVEOLAS(ve).totalSupplyLockedAtT(ts); } else { revert WrongTimestamp(sPoint.ts, ts); } }
function totalSupplyLockedAtT(uint256 ts) external view returns (uint256 vPower) { uint256 numPoints = IVEOLAS(ve).totalNumPoints(); if (numPoints == 0) { revert NoSupplyPoints(); } PointVoting memory sPoint = IVEOLAS(ve).mapSupplyPoints(numPoints - 1); if (ts < sPoint.ts) { revert WrongTimestamp(sPoint.ts, ts); } vPower = IVEOLAS(ve).totalSupplyLockedAtT(ts); }

Verify Data with Correct Chain Id:

Check that the chain Id provided is supported and matches the expected value for the target and selector.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L189C5-L202C6

function _verifyData(address target, bytes memory data, uint256 chainId) internal { require(chainId == block.chainid, "Chain Id does not match current chain"); // Existing logic... }

This validation ensures that the data is verified against the correct chain Id, preventing cross-chain vulnerabilities and ensuring that governance actions are executed in the correct context.

Prevent Unauthorized Target and Selector Combinations:

Ensure that only authorized combinations of target addresses and function selectors are allowed.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L441C5-L456C1

function setTargetSelectorChainIds( address[] memory targets, bytes4[] memory selectors, uint256[] memory chainIds, bool[] memory statuses ) external { require(msg.sender == owner, "OwnerOnly"); // Existing logic... for (uint256 i = 0; i < targets.length; ++i) { require(targets[i] != address(0), "Target address cannot be zero"); require(selectors[i] != bytes4(0), "Selector cannot be zero"); // Existing logic... } }

This check ensures that only authorized combinations of target addresses and function selectors are allowed, preventing unauthorized governance actions.

Validate Bridged Data for Correct Chain Id:

Ensure that the bridged data is for the correct chain Id and that the bridge mediator addresses match.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L209C5-L223C14

function _verifyBridgedData(bytes memory data, uint256 chainId) internal { require(chainId == block.chainid, "Chain Id does not match current chain"); }

This validation ensures that the bridged data is processed for the correct chain Id, preventing cross-chain vulnerabilities and ensuring that governance actions are executed in the correct context.

Enforce Unique Bridge Mediators:

Ensure that each L1 bridge mediator is mapped to a unique L2 bridge mediator and chain ID to prevent misconfiguration.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L495C5-L508C10

function setBridgeMediatorChainIds( address[] memory bridgeMediatorL1s, address[] memory bridgeMediatorL2s, uint256[] memory chainIds ) external { require(msg.sender == owner, "OwnerOnly"); for (uint256 i = 0; i < bridgeMediatorL1s.length; ++i) { require(mapBridgeMediatorL1L2ChainIds[bridgeMediatorL1s[i]] == 0, "Bridge mediator already set"); } }

This check ensures that each L1 bridge mediator is uniquely mapped, preventing potential errors in cross-chain communication.

Validate Governor State:

Ensure that the governor's state is checked against a valid proposal ID to prevent misuse of the pause function.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L539C5-L554C10

function pause() external { require(msg.sender == owner || msg.sender == multisig, "Not authorized to pause"); if (msg.sender == multisig) { ProposalState state = IGovernor(governor).state(governorCheckProposalId); require(state == ProposalState.Defeated, "Governor proposal not defeated"); } paused = 2; emit GuardPaused(msg.sender); }

This validation ensures that the multisig can only pause the guard if the governance is considered inactive, based on a defeated proposal, maintaining the integrity of the governance process.

Check Array Lengths for Consistency:

Ensure that the lengths of input arrays are consistent to prevent logic errors in setting target selectors and chain IDs.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L441C5-L456C1

function setTargetSelectorChainIds( address[] memory targets, bytes4[] memory selectors, uint256[] memory chainIds, bool[] memory statuses ) external { require(msg.sender == owner, "OwnerOnly"); require(targets.length == selectors.length && targets.length == chainIds.length && targets.length == statuses.length, "Array lengths mismatch"); // Existing logic... }

This check ensures that the lengths of all input arrays are consistent, preventing potential errors when authorizing target-selector-chainId combinations.

Validate Data Length for Bridged Data:

Ensure that the data length for bridged data is sufficient to contain all necessary information.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L209C5-L246C6

function _verifyBridgedData(bytes memory data, uint256 chainId) internal { require(data.length >= MIN_BRIDGE_DATA_LENGTH, "Bridged data length insufficient"); // Existing logic... }

This validation ensures that the bridged data contains all the necessary information for processing, preventing errors due to insufficient data length.

Optimize Key Construction for Mappings

The contract constructs a single uint256 key for the mapAllowedTargetSelectorChainIds mapping by combining an address, a selector, and a chain ID. This approach is error-prone and could lead to key collisions or make it difficult to manage the keys.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/multisigs/GuardCM.sol#L192C9-L197C1

uint256 targetSelectorChainId = uint256(uint160(target)); targetSelectorChainId |= uint256(uint32(bytes4(data))) << 160; targetSelectorChainId |= chainId << 192;

Mitigation:

Use a struct to represent the combined values and map it to the desired value instead of combining them into a single uint256. This will make the code more readable and reduce the risk of key collisions.

struct TargetSelectorChainId { address target; bytes4 selector; uint64 chainId; } mapping(TargetSelectorChainId => bool) public mapAllowedTargetSelectorChainIds;

Check Message Sender is FxChild in Message Processing:

Ensure that the message sender is the authorized FxChild address when processing messages from the root.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxGovernorTunnel.sol#L107C5-L169C2

function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes memory data) external override { require(msg.sender == fxChild, "Caller is not the FxChild"); // Existing logic... }

This check ensures that only messages sent by the FxChild contract are processed, preventing unauthorized entities from triggering the message processing function.

Check Root Message Sender is RootGovernor in Message Processing:

Ensure that the root message sender is the authorized RootGovernor address when processing messages from the root.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxGovernorTunnel.sol#L107C5-L169C2

function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes memory data) external override { require(rootMessageSender == rootGovernor, "Caller is not the RootGovernor"); // Existing logic... }

This check ensures that only messages sent by the RootGovernor are processed, maintaining the integrity of cross-chain governance actions.

Event for Governor Change

Emit an event when the rootGovernor is changed for better transparency and traceability.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxGovernorTunnel.sol#L81C5-L95C6

event RootGovernorChanged(address indexed previousGovernor, address indexed newGovernor); function changeRootGovernor(address newRootGovernor) external { // existing checks emit RootGovernorChanged(rootGovernor, newRootGovernor); rootGovernor = newRootGovernor; // existing code }

Substitute Function Modifiers for Checks

Use function modifiers to encapsulate repetitive checks, improving code readability and maintainability.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxGovernorTunnel.sol#L107C5-L169C2

modifier onlyFxChild() { if (msg.sender != fxChild) { revert FxChildOnly(msg.sender, fxChild); } _; } modifier onlyRootGovernor(address rootMessageSender) { if (rootMessageSender != rootGovernor) { revert RootGovernorOnly(rootMessageSender, rootGovernor); } _; } function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes memory data) external override onlyFxChild onlyRootGovernor(rootMessageSender) noReentrancy { // existing code }

Denial of Service (DoS) - Unbounded Loops

The processMessageFromRoot function processes an array of transactions in a loop without a limit on the number of transactions, which could lead to out-of-gas errors if the message contains too many transactions.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxGovernorTunnel.sol#L107C5-L169C2

for (uint256 i = 0; i < dataLength;) { // existing code }

Optimization:

uint256 public constant MAX_TRANSACTIONS_PER_CALL = 10; // Example value uint256 transactionCount = 0; for (uint256 i = 0; i < dataLength && transactionCount < MAX_TRANSACTIONS_PER_CALL;) { // existing code transactionCount++; }

Introducing a limit on the number of transactions processed in a single call prevents potential out-of-gas errors due to too many transactions.

ETH-Transfer - Sending Ether can be optimized

The contract sends Ether using a low-level call, which is prone to reentrancy attacks and does not limit the gas sent along with the Ether.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxGovernorTunnel.sol#L160

(bool success, ) = target.call{value: value}(payload);

Optimization:

(bool success, ) = target.call{value: value, gas: MAX_GAS_PER_TRANSACTION}(payload); require(success, "ETH transfer failed");

Limiting the gas sent with the Ether transfer reduces the risk of reentrancy attacks and ensure that the contract does not run out of gas during the transfer.

Check Message Sender is AMBContractProxyHome in Message Processing

Check that the message sender is the authorized AMBContractProxyHome address when processing messages from the foreign network.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

function processMessageFromForeign(bytes memory data) external { require(msg.sender == AMBContractProxyHome, "Caller is not the AMBContractProxyHome"); }

This check ensures that only messages sent by the AMBContractProxyHome contract are processed, preventing unauthorized entities from triggering the message processing function.

Check Foreign Governor in Message Processing:

Check that the foreign governor address is the authorized sender when processing messages from the foreign network.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

function processMessageFromForeign(bytes memory data) external { address bridgeGovernor = IAMB(AMBContractProxyHome).messageSender(); require(bridgeGovernor == foreignGovernor, "Caller is not the ForeignGovernor"); // Existing logic... }

This check ensures that only messages sent by the ForeignGovernor are processed, maintaining the integrity of cross-chain governance actions.

Validate Data Length in Message Processing:

Check that the data length is sufficient to contain all necessary information for message processing.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

function processMessageFromForeign(bytes memory data) external { require(data.length >= DEFAULT_DATA_LENGTH, "Data length below required minimum"); // Existing logic... }

This validation ensures that the data contains all the necessary information for processing, preventing errors due to insufficient data length.

Revert on Failed Execution of Target Calls:

Ensure that the contract reverts if the execution of the target call fails.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

function processMessageFromForeign(bytes memory data) external { // Unpack data... (bool success, ) = target.call{value: value}(payload); require(success, "Execution of target call failed"); }

This check ensures that if the target execution fails, the entire transaction is reverted, maintaining the atomicity of operations and preventing partial state changes

Validate Payload Length:

Ensure that the payload length does not exceed the remaining data length to prevent buffer overflows.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

// Check that the payload length does not exceed the remaining data length. if (i + payloadLength > dataLength) { revert IncorrectDataLength(dataLength - i, payloadLength); }

Event Emission After State Changes:

Emit events after state changes are finalized to follow the best practice of logging the final state.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

// Emit the MessageReceived event after all state changes and interactions. emit MessageReceived(governor, data);

Optimize Data Unpacking

The contract uses inline assembly for data unpacking, which is less readable and more error-prone than using Solidity's built-in functions.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L105C5-L169C2

// @audit Use Solidity's built-in abi.decode for better readability and safety. (address target, uint96 value, uint32 payloadLength) = abi.decode(data, (address, uint96, uint32));

Using abi.decode improves code readability and reduces the risk of errors associated with inline assembly.

Unchecked External Calls

The contract makes external calls using the low-level call function, which can be risky if the target contract is malicious or has fallback functions that can cause unexpected behavior.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/HomeMediator.sol#L160C13-L164C10

(bool success, ) = target.call{value: value}(payload); if (!success) { revert TargetExecFailed(target, value, payload); }

Use a try-catch block to handle failed calls more gracefully.

  1. Centralization of Control

The contract's mint and burn functions are controlled by a single owner address, which introduces a central point of failure or abuse.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/BridgedERC20.sol#L48C5-L55C6

function mint(address account, uint256 amount) external { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // ... }

Implement a decentralized control mechanism, such as a multisig or a DAO, to manage minting and burning.

Owner Authentication

The contract uses a simple owner check for privileged functions. This could be improved by using a modifier for reusability and clarity.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/BridgedERC20.sol#L30C5-L44C1

function changeOwner(address newOwner) external { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // ... }
modifier onlyOwner() { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } _; } function changeOwner(address newOwner) external onlyOwner { // ... }

Using a modifier for owner checks reduces code duplication and makes the contract more readable.

Move Zero Address Check logic to Modifier for Reusability:

The zero address check is repeated in multiple functions. This can also be moved to a modifier for better code reuse.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/BridgedERC20.sol#L30C5-L44C1

function changeOwner(address newOwner) external { if (newOwner == address(0)) { revert ZeroAddress(); } // ... }

change:

modifier notZeroAddress(address _address) { if (_address == address(0)) { revert ZeroAddress(); } _; } function changeOwner(address newOwner) external onlyOwner notZeroAddress(newOwner) { // ... }

A modifier centralizes the zero address check, making the code cleaner and reducing the chance of missing this check in future functions.

Front-running Ownership Transfer

A malicious actor could front-run the changeOwner transaction to change the owner to an address they control, potentially causing a DoS.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/BridgedERC20.sol#L30C5-L44C1

function changeOwner(address newOwner) external onlyOwner notZeroAddress(newOwner) { owner = newOwner; emit OwnerUpdated(newOwner); }

Mitigation:

Implement a commit-reveal scheme or a time lock to prevent front-running.

Lack of Authorization Checks for Deposit Functions

The deposit and depositTo functions do not have explicit checks to ensure that only authorized entities (like the bridge mediator) can call them. This could potentially allow unauthorized minting of tokens on L1 if the L2 contract is not properly secured.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20ChildTunnel.sol#L50C5-L64C6

function deposit(uint256 amount) external { _deposit(msg.sender, amount); }

Implement authorization checks to ensure that only the bridge mediator can initiate deposits.

Event Emission After State Changes

Events should be emitted after state changes to follow the checks-effects-interactions pattern.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20ChildTunnel.sol#L79

// Deposit tokens on an L2 bridge contract (lock) bool success = IERC20(childToken).transferFrom(msg.sender, address(this), amount); if (!success) { revert TransferFailed(childToken, msg.sender, address(this), amount); } // Emit event after state change emit FxDepositERC20(childToken, rootToken, msg.sender, to, amount);

Emitting events after state changes ensures that the log reflects the final state, which is important for off-chain services and block explorers that rely on event logs.

Event Arguments Ordering

For better indexing and searching, it's recommended to order event arguments from the most to the least likely to be used as filter parameters.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20ChildTunnel.sol#L25

event FxDepositERC20(address indexed from, address indexed to, address childToken, address rootToken, uint256 amount);

This ordering allows for more efficient filtering by the most relevant parameters, such as the sender and recipient addresses.

Use OpenZeppelin's SafeERC20 library for safer token transfers.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20RootTunnel.sol#L86C5-L108C2

// @audit At the top of the contract, import OpenZeppelin's SafeERC20 library import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; // @audit Use SafeERC20 for IERC20 using SafeERC20 for IERC20; // @audit Use safeTransferFrom and safeTransfer in the _withdraw function function _withdraw(address to, uint256 amount) internal { // ... IERC20(rootToken).safeTransferFrom(msg.sender, address(this), amount); // ... IERC20(rootToken).safeTransfer(to, amount); }

The SafeERC20 library provides wrappers around ERC20 operations that throw on failure, which is safer than using raw calls. This helps to prevent issues when interacting with poorly implemented ERC20 tokens.

Implement Check Effects Interactions Pattern in _withdraw function

Follow the Checks-Effects-Interactions pattern to prevent reordering of operations that could lead to vulnerabilities.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20RootTunnel.sol#L86C5-L108C2

function _withdraw(address to, uint256 amount) internal { // Checks if (amount == 0) { revert ZeroValue(); } // Effects IERC20(rootToken).burn(amount); // Interactions bytes memory message = abi.encode(msg.sender, to, amount); _sendMessageToChild(message); bool success = IERC20(rootToken).transferFrom(msg.sender, address(this), amount); if (!success) { revert TransferFailed(rootToken, msg.sender, address(this), amount); } emit FxWithdrawERC20(rootToken, childToken, msg.sender, to, amount); }

Ensuring that all checks and state changes (effects) are done before external calls (interactions) is good practice and can prevent reentrancy and state inconsistencies.

Validate State Changes

Ensure that state changes are valid and expected, especially after external calls.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20RootTunnel.sol#L72C5-L80C6

function _processMessageFromChild(bytes memory message) internal override { // ... uint256 balanceBefore = IERC20(rootToken).balanceOf(to); IERC20(rootToken).mint(to, amount); uint256 balanceAfter = IERC20(rootToken).balanceOf(to); require(balanceAfter - balanceBefore == amount, "Minting did not occur as expected"); // ... }

Checking the token balance before and after minting ensures that the minting process had the expected effect on the token balance.

Use safeTransferFrom instead of transferFrom

The contract assumes that the transferFrom function of the IERC20 token will always succeed. If these functions fail (due to a malicious token contract or other issues), it could lead to a DoS condition.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/governance/contracts/bridges/FxERC20RootTunnel.sol#L98C9-L101C10

bool success = IERC20(rootToken).transferFrom(msg.sender, address(this), amount); if (!success) { revert TransferFailed(rootToken, msg.sender, address(this), amount); }
IERC20(rootToken).safeTransferFrom(msg.sender, address(this), amount);

Using the safeTransferFrom function from OpenZeppelin's SafeERC20 library ensures that these operations revert only if the transfer fails. This prevents a malicious or faulty token contract from causing a DoS.

Centralization Risks

The contract's administrative functions such as changeOwner, changeManager, and setBaseURI are controlled by a single owner address, which introduces a central point of failure or abuse.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericRegistry.sol#L37C5-L50C6

function changeOwner(address newOwner) external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // ... }

Consider implementing a decentralized control mechanism, such as a multisig or a DAO, to manage these administrative functions.

Lack of Input Validation

The tokenByIndex function assumes that the id provided is valid and does not perform any checks to ensure that the resulting unitId is within the valid range of existing tokens.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericRegistry.sol#L97C5-L102C6

function tokenByIndex(uint256 id) external view virtual returns (uint256 unitId) { unitId = id + 1; if (unitId > totalSupply) { revert Overflow(unitId, totalSupply); } }

Add input validation to ensure that the id provided will result in a valid unitId.

function tokenByIndex(uint256 id) external view virtual returns (uint256 unitId) { require(id < totalSupply, "Invalid id"); unitId = id + 1; }

Validate Token ID in tokenURI

Ensure that the unitId provided to the tokenURI function corresponds to an existing token.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericRegistry.sol#L135C5-L141C6

function tokenURI(uint256 unitId) public view virtual override returns (string memory) { // ... }

Change:

function tokenURI(uint256 unitId) public view virtual override returns (string memory) { require(_exists(unitId), "ERC721Metadata: URI query for nonexistent token"); // ... }

Validate Base URI in setBaseURI

Ensure that the new base URI is not empty.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericRegistry.sol#L78C5-L91C6

function setBaseURI(string memory bURI) external virtual { require(bytes(bURI).length > 0, "GenericRegistry: Base URI cannot be empty"); // ... }

Validate Token Existence in _getUnitHash

The _getUnitHash function is meant to be called with existing token IDs, ensure that the unitId is valid.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericRegistry.sol#L129

function _getUnitHash(uint256 unitId) internal view virtual returns (bytes32);
function _getUnitHash(uint256 unitId) internal view virtual returns (bytes32) { require(_exists(unitId), "GenericRegistry: Hash query for nonexistent token"); // ... }

Access Control with Role Management:

Implement a role-based access control mechanism, such as OpenZeppelin's AccessControl, to manage the manager role more securely.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L49C5-L61C10

import "@openzeppelin/contracts/access/AccessControl.sol"; contract UnitRegistry is GenericRegistry, AccessControl { // @audit Define a manager role bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); constructor(UnitType _unitType) { _setupRole(MANAGER_ROLE, msg.sender); } function create(...) external virtual returns (uint256 unitId) { // Check for the manager privilege for a unit creation require(hasRole(MANAGER_ROLE, msg.sender), "Caller is not a manager"); // ... } }

Using a role-based access control system provides flexibility and security in managing permissions.

Error Handling with Custom Errors

Use custom errors instead of revert strings to save gas and provide clearer error messages.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L49C5-L61C10

error ManagerOnly(address caller); error ZeroAddress(); error ZeroValue(); // ... function create(...) external virtual returns (uint256 unitId) { // ... if (manager != msg.sender) { revert ManagerOnly(msg.sender); } // ... }

Custom errors are more gas-efficient than revert strings and can carry additional information.

Event Emission After State Changes

Ensure that events are emitted after all state changes to prevent any inconsistencies between emitted events and contract state.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L49C5-L61C10

function create(...) external virtual returns (uint256 unitId) { // ... _safeMint(unitOwner, unitId); emit CreateUnit(unitId, unitType, unitHash); // ... }

Emitting events after state changes ensures that the event data reflects the final state.

Unbounded Loops in _calculateSubComponents

The _calculateSubComponents function contains loops that could potentially iterate over a large number of elements, leading to high gas costs and possible DoS if the transaction exceeds the block gas limit.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L200C5-L265C6

for (uint32 i = 0; i < numUnits; ++i) { maxNumComponents += numComponents[i]; } for (counter = 0; counter < maxNumComponents; ++counter) { }
After:
// Introduce a constant to limit the maximum number of dependencies uint32 public constant MAX_DEPENDENCIES = 10; function _calculateSubComponents(...) internal view virtual returns (uint32[] memory subComponentIds) { require(unitIds.length <= MAX_DEPENDENCIES, "Exceeds maximum dependencies"); // ... }

Introducing a maximum limit on dependencies can prevent unbounded loops and ensure that the function does not consume excessive gas.

Unchecked Array Push in updateHash

The updateHash function uses an unchecked push operation on an array, which could grow indefinitely and make the function expensive to call.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L121

function updateHash(...) external virtual returns (bool success) { // ... mapUnitIdHashes[unitId].push(unitHash); // ... }
After:
// Introduce a constant to limit the number of hash updates uint256 public constant MAX_HASH_UPDATES = 10; function updateHash(...) external virtual returns (bool success) { require(mapUnitIdHashes[unitId].length < MAX_HASH_UPDATES, "Exceeds max hash updates"); mapUnitIdHashes[unitId].push(unitHash); // ... }

Limiting the number of times a unit's hash can be updated prevents the array from growing indefinitely and ensure that the gas cost remains manageable.

Unchecked Array Growth in create

The create function allows for an unbounded number of subcomponents to be added to mapSubComponents.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L49

function create(...) external virtual returns (uint256 unitId) { // ... mapSubComponents[unitId] = subComponentIds; // ... }
After:
// @audit Introduce a constant to limit the number of subcomponents uint256 public constant MAX_SUBCOMPONENTS = 50; function create(...) external virtual returns (uint256 unitId) { // ... require(subComponentIds.length <= MAX_SUBCOMPONENTS, "Exceeds max subcomponents"); mapSubComponents[unitId] = subComponentIds; // ... }

By limiting the number of subcomponents, we prevent the mapSubComponents mapping from becoming too large to iterate over, which could lead to excessive gas costs.

Resource Exhaustion in getLocalSubComponents

The getLocalSubComponents function could potentially return a very large array, which could cause the function to fail due to out-of-gas errors.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L182

function getLocalSubComponents(uint256 unitId) external view returns (uint32[] memory subComponentIds, uint256 numSubComponents) { // ... }

Ensuring that the number of subcomponents is limited as mentioned earlier will prevent this function from running out of gas.

Denial of Service (DoS) - Unbounded Loops in _calculateSubComponents

The _calculateSubComponents function contains loops that could iterate over a large number of elements, potentially leading to high gas costs and DoS.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/UnitRegistry.sol#L200C5-L265C6

for (uint32 i = 0; i < numUnits; ++i) { // ... maxNumComponents += numComponents[i]; } // ... for (counter = 0; counter < maxNumComponents; ++counter) { // ... }
Optimization:
uint32 public constant MAX_DEPENDENCIES = 10; function _calculateSubComponents(...) internal view virtual returns (uint32[] memory subComponentIds) { require(unitIds.length <= MAX_DEPENDENCIES, "Exceeds maximum dependencies"); // ... }

Introducing a maximum limit on dependencies prevents unbounded loops and ensures that the function does not consume excessive gas.

Unbounded Loops and Gas Limitations

The _checkDependencies function iterates over an array without any bounds on its size. This could be exploited by a malicious actor to create a Denial of Service (DoS) attack by submitting a very large array of dependencies, potentially causing the function to run out of gas and fail.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/ComponentRegistry.sol#L27C5-L35C6

function _checkDependencies(uint32[] memory dependencies, uint32 maxComponentId) internal virtual override { uint32 lastId; for (uint256 iDep = 0; iDep < dependencies.length; ++iDep) { if (dependencies[iDep] < (lastId + 1) || dependencies[iDep] > maxComponentId) { revert ComponentNotFound(dependencies[iDep]); } lastId = dependencies[iDep]; } }

Mitigation:

Implement a limit on the size of the dependencies array to prevent excessively large arrays from being processed.

Unchecked Loop Length in _checkDependencies Function:

The loop iterates over the dependencies array without any constraints on its length. An attacker could submit a very large dependencies array, which could cause the transaction to run out of gas and revert, leading to a DoS condition. This is especially problematic if the function is part of a critical workflow, such as registering new agents.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/AgentRegistry.sol#L31C5-L45C10

for (uint256 iDep = 0; iDep < dependencies.length; ++iDep) { if (dependencies[iDep] < (lastId + 1) || dependencies[iDep] > componentTotalSupply) { revert ComponentNotFound(dependencies[iDep]); } lastId = dependencies[iDep]; }

Mitigation:

Implement a limit on the maximum number of dependencies that can be processed in a single transaction. This would prevent an attacker from submitting excessively large arrays that could exhaust the gas limit.

Lack of Input Validation in calculateSubComponents:

The calculateSubComponents function does not perform any validation on the input unitIds. If the function is used in a critical process, an attacker could pass an array with invalid or maliciously crafted unitIds that could lead to incorrect calculations or excessive gas consumption.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/AgentRegistry.sol#L70

function calculateSubComponents(uint32[] memory unitIds) external view returns (uint32[] memory subComponentIds) { subComponentIds = _calculateSubComponents(UnitType.Agent, unitIds); }

Mitigation:

Implement input validation checks to ensure that the unitIds passed to the function are within an acceptable range and conform to expected formats or constraints. This could include checking for duplicates, ensuring the IDs exist, or enforcing a maximum array size.

Two step ownership transfer process

The function allows the current owner to transfer ownership to a new address. However, if the current owner's account is compromised, the attacker can transfer ownership to their address, gaining full control over the contract.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericManager.sol#L20

function changeOwner(address newOwner) external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } if (newOwner == address(0)) { revert ZeroAddress(); } owner = newOwner; emit OwnerUpdated(newOwner); }

The changeOwner function lacks a mechanism to prevent an attacker from immediately taking over the contract if they gain access to the owner's private key. Once the ownership is transferred, the attacker can pause the contract, preventing legitimate users from interacting with it, or make other unauthorized changes.

Mitigation:

Implement a two-step ownership transfer process.

Redundant calls can be avoided to save gas

The lack of checks in the pause and unpause functions lead to redundant calls that do not change the state of the contract.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/GenericManager.sol#L36

function pause() external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } paused = true; emit Pause(msg.sender); } function unpause() external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } paused = false; emit Unpause(msg.sender); }

The pause and unpause functions do not check whether the contract is already in the desired state (paused or unpaused). This means that if the contract is already paused, calling pause again will still emit an event and use gas, even though the state of the contract does not change. The same applies to the unpause function. Malicious actors could exploit this by repeatedly calling these functions to drain funds from the contract owner's address through gas fees.

Mitigation:

Add a check to ensure that the contract's state is only changed when necessary, and prevent unnecessary gas expenditure and event emissions.

function pause() external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } if (paused) { revert AlreadyPaused(); } paused = true; emit Pause(msg.sender); } function unpause() external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } if (!paused) { revert NotPaused(); } paused = false; emit Unpause(msg.sender); }

Lack of Access Control on sensitive functions

The create and updateHash functions do not have any access control mechanisms in place. This means that any user can call these functions and potentially create or update components or agents without proper authorization.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/RegistriesManager.sol#L27

function create(...) external returns (uint256 unitId) { ... } function updateHash(...) external returns (bool success) { ... }
After:
modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function create(...) external onlyOwner returns (uint256 unitId) { ... } function updateHash(...) external onlyOwner returns (bool success) { ... }

Adding an onlyOwner modifier ensures that only the owner of the contract can create or update components or agents, providing a basic level of access control.

Unchecked External Calls

The contract makes external calls to IRegistry without checking the return value for the create function. It's important to handle the possibility of these calls failing.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/RegistriesManager.sol#L39

unitId = IRegistry(componentRegistry).create(unitOwner, unitHash, dependencies);
(bool success, bytes memory data) = componentRegistry.call( abi.encodeWithSelector( IRegistry.create.selector, unitOwner, unitHash, dependencies ) ); require(success && data.length > 0, "External call to create failed"); unitId = abi.decode(data, (uint256));

Using a low-level call with proper checks ensures that the external call to create is successful and the return value is valid.

Contract Governance Limitations

The contract has an owner state variable that is set to msg.sender in the constructor, but there is no functionality to transfer ownership or to renounce ownership. This limits the flexibility and future governance of the contract.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/RegistriesManager.sol#L39

constructor(address _componentRegistry, address _agentRegistry) { // ... owner = msg.sender; }

changes:

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "New owner is the zero address"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; }

Adding an OwnershipTransferred event and a transferOwnership function allows the current owner to transfer control of the contract to a new owner, improving governance.

Error Handling

The contract does not emit events for critical actions such as the creation or update of components or agents. Events are important for off-chain clients to track the state changes of the contract.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/RegistriesManager.sol#L50

event ComponentCreated(uint256 indexed unitId, address indexed unitOwner, bytes32 unitHash, uint32[] dependencies); event HashUpdated(IRegistry.UnitType indexed unitType, uint256 indexed unitId, bytes32 unitHash); // Emit events in the respective functions emit ComponentCreated(unitId, unitOwner, unitHash, dependencies); emit HashUpdated(unitType, unitId, unitHash);

Emitting events after creating a component or updating a hash allows off-chain clients to monitor these actions and react accordingly.

Unchecked External Call

The contract makes an external call to the IGnosisSafeProxyFactory without proper checks to ensure that the call was successful. If the call to createProxyWithNonce fails, the returned address could be the zero address, which would indicate that the proxy contract was not created successfully. However, the current implementation does not handle this scenario, and the multisig variable would be set to the zero address, potentially leading to loss of funds or other unintended behavior.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/multisigs/GnosisSafeMultisig.sol#L106

multisig = IGnosisSafeProxyFactory(gnosisSafeProxyFactory).createProxyWithNonce(gnosisSafe, safeParams, nonce);

Mitigation:

Check Return Value and Use require

To mitigate this issue, the contract should check the return value of the external call and use a require statement to ensure that the returned address is not the zero address. This would prevent the multisig variable from being set to an invalid address and provide a clear error message if the proxy creation fails.

Change:

address proxy = IGnosisSafeProxyFactory(gnosisSafeProxyFactory).createProxyWithNonce(gnosisSafe, safeParams, nonce); require(proxy != address(0), "Proxy creation failed"); multisig = proxy;

Loop Optimization for Gas Efficiency

The contract manually copies bytes from data to payload in a loop. This can be optimized using Solidity's built-in functions.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/multisigs/GnosisSafeSameAddressMultisig.sol#L112C13-L115C14

bytes memory payload = new bytes(payloadLength); for (uint256 i = 0; i < payloadLength; ++i) { payload[i] = data[i + DEFAULT_DATA_LENGTH]; }

After:

bytes memory payload = new bytes(payloadLength); assembly { extcodecopy(multisig, add(payload, 32), DEFAULT_DATA_LENGTH, payloadLength) }

Using extcodecopy in assembly can copy the bytecode directly from the address, which is more gas-efficient than a manual loop in Solidity.

Low-Level Call to External Contract

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/multisigs/GnosisSafeSameAddressMultisig.sol#L118C13-L122C10

(bool success, ) = multisig.call(payload); if (!success) { revert MultisigExecFailed(multisig); }

In the snippet above, the contract sends an arbitrary payload to the multisig address using a low-level call. This is risky because:

Reentrancy: If the multisig contract is malicious or has a vulnerability, it could call back into the current contract before the initial operation is completed. This could lead to unexpected behavior, especially if the state is changed after the call. Gas Stipends: The .call method forwards all available gas by default, which can lead to out-of-gas errors if the called contract consumes a lot of gas. Lack of Return Data: The low-level call does not return any data, which means that if the multisig contract function called returns important information, it will not be accessible.

Mitigation: Use Direct Function Calls To mitigate this issue, the contract should use direct function calls to known interfaces instead of low-level calls. This ensures that only the intended function can be called, and it's clearer what the expected behavior is. Additionally, direct function calls automatically check for the success of the call and revert if the call fails, which simplifies error handling.

// Assuming IGnosisSafe interface has the execTransaction function IGnosisSafe gnosisSafe = IGnosisSafe(multisig); (bool success, ) = gnosisSafe.execTransaction(payload); if (!success) { revert MultisigExecFailed(multisig); }

By using the interface's execTransaction function directly, the contract benefits from the Solidity compiler's checks and reverts on failure. This approach also allows the contract to handle any return values from the function call, which can be useful for further validation or logic.

Assumption on Data Format and Content

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/registries/contracts/multisigs/GnosisSafeSameAddressMultisig.sol#L85

function create( address[] memory owners, uint256 threshold, bytes memory data ) external returns (address multisig) { // ... assembly { multisig := mload(add(data, DEFAULT_DATA_LENGTH)) } // ... }

The contract uses inline assembly to load the multisig address from the data parameter. The issues with this approach are:

Data Integrity: The contract assumes that the data parameter is correctly formatted and that the first 20 bytes indeed represent a valid address. If the data is malformed or intentionally crafted to mislead, it could lead to incorrect behavior or security vulnerabilities.

Validation: There is no validation on the data parameter beyond checking its length. Malicious actors could potentially pass in data that meets the length requirement but contains a malicious address or payload.

Validate and Decode Data Properly To mitigate this issue, the contract should validate and decode the data parameter using Solidity's built-in functions, which are designed to handle such operations safely.

function create( address[] memory owners, uint256 threshold, bytes memory data ) external returns (address multisig) { if (data.length < DEFAULT_DATA_LENGTH) { revert IncorrectDataLength(DEFAULT_DATA_LENGTH, data.length); } // @audit Decode the address from the data parameter multisig = abi.decode(data[:DEFAULT_DATA_LENGTH], (address)); }

Using abi.decode ensures the contract can safely extract the address from the data parameter and ensure that it is properly formatted. This approach also provides better readability and maintainability of the code.

Unchecked External Calls

The deposit function makes an external call to ITreasury(treasury).depositTokenForOLAS, which could fail and revert the entire transaction.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Depository.sol

function deposit(uint256 productId, uint256 tokenAmount) external returns (uint256 payout, uint256 maturity, uint256 bondId) { // ... existing code ... ITreasury(treasury).depositTokenForOLAS(msg.sender, tokenAmount, token, payout); // ... existing code ... }

Use a try-catch block to handle potential reverts from the external call.

function deposit(uint256 productId, uint256 tokenAmount) external returns (uint256 payout, uint256 maturity, uint256 bondId) { // ... existing code ... try ITreasury(treasury).depositTokenForOLAS(msg.sender, tokenAmount, token, payout) { // Handle success } catch { // Handle failure, e.g., log an event or revert with a custom error message revert("Treasury deposit failed"); } // ... existing code ... }

Timestamp Dependence

The contract uses block.timestamp for calculating bond maturity, which can be slightly manipulated by miners.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Depository.sol

function deposit(uint256 productId, uint256 tokenAmount) external returns (uint256 payout, uint256 maturity, uint256 bondId) { // ... existing code ... maturity = block.timestamp + product.vesting; // ... existing code ... }

While the risk is minimal for long-term vesting periods, it's good practice to document the assumption and consider using an oracle for time-sensitive logic if necessary.

Use of transfer without checking return value

The redeem function transfers tokens without checking the return value.\

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Depository.sol

function redeem(uint256[] memory bondIds) external returns (uint256 payout) { // ... existing code ... IToken(olas).transfer(msg.sender, payout); }

Use transfer with a require statement to ensure the transfer was successful or use safeTransfer from OpenZeppelin's SafeERC20 library.

Gas optimization

The redeem function reads from storage multiple times within a loop, which is costly.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Depository.sol

function redeem(uint256[] memory bondIds) external returns (uint256 payout) { for (uint256 i = 0; i < bondIds.length; ++i) { uint256 pay = mapUserBonds[bondIds[i]].payout; } }

Cache the storage variable in memory before the loop to save gas.

function redeem(uint256[] memory bondIds) external returns (uint256 payout) { for (uint256 i = 0; i < bondIds.length; ++i) { Bond memory bond = mapUserBonds[bondIds[i]]; uint256 pay = bond.payout; // ... existing code ... } // ... existing code ... }

Gas Limit DoS

The redeem function allows a user to redeem multiple bonds in a single transaction. If a user has a large number of bonds, the transaction could run out of gas, effectively locking the user's funds because they are unable to redeem their bonds.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Depository.sol

function redeem(uint256[] memory bondIds) external returns (uint256 payout) { for (uint256 i = 0; i < bondIds.length; ++i) { // ... existing code ... } // ... existing code ... }

Limit the number of bonds that can be redeemed in a single transaction. Alternatively, implement a paginated redemption mechanism that allows users to redeem their bonds in batches.

Front-Running Attacks

The deposit function calculates the payout based on the current price of LP tokens. A malicious actor could observe a pending transaction and execute a transaction with a higher gas price to affect the LP token price before the deposit transaction is confirmed.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Depository.sol

function deposit(uint256 productId, uint256 tokenAmount) external returns (uint256 payout, uint256 maturity, uint256 bondId) { // ... existing code ... payout = IGenericBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP); // ... existing code ... }

Implement commit-reveal schemes or use an oracle to provide a time-weighted average price (TWAP) to mitigate front-running attacks.

Unauthorized State Changes

The changeOwner, changeManagers, and changeBondCalculator functions allow the owner to change critical contract addresses. If the owner's private key is compromised, an attacker could redirect funds or disrupt the contract's functionality.

Lines of code

function changeOwner(address newOwner) external { // ... existing code ... owner = newOwner; // ... existing code ... }

Implement a multi-signature mechanism or a time-locked admin function to change critical contract parameters. This would require multiple confirmations before such changes take effect, reducing the risk of a single point of failure.

Unchecked External Calls

The contract makes external calls to other contracts, which can be manipulated by malicious actors if those contracts are not trustworthy or have vulnerabilities.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Dispenser.sol

(reward, topUp) = ITokenomics(tokenomics).accountOwnerIncentives(msg.sender, unitTypes, unitIds); ... success = ITreasury(treasury).withdrawToAccount(msg.sender, reward, topUp);

External calls can lead to several issues, such as reentrancy attacks or unexpected failures if the called contract throws an error or runs out of gas.

Use try/catch to handle potential errors from external calls gracefully and ensure that the state of the contract remains consistent.

try ITokenomics(tokenomics).accountOwnerIncentives(msg.sender, unitTypes, unitIds) returns (uint256 _reward, uint256 _topUp) { reward = _reward; topUp = _topUp; } catch { revert ExternalCallFailed("Tokenomics"); } ... try ITreasury(treasury).withdrawToAccount(msg.sender, reward, topUp) { success = true; } catch { revert ExternalCallFailed("Treasury"); }

Implement Two Step Ownership Transfer

The contract allows the owner to transfer ownership to a new address. If the owner's private key is compromised, the attacker can transfer ownership to themselves.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Dispenser.sol

function changeOwner(address newOwner) external { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } ... owner = newOwner; emit OwnerUpdated(newOwner); }

Implement a two-step ownership transfer process. This can prevent accidental transfers and provide a recovery window if the owner's key is compromised.

Reliance on External Contract State

The calculatePayoutOLAS function relies on the state of an external contract (ITokenomics) to fetch the last discount factor (IDF). If the external contract is compromised or behaves unexpectedly, it could affect the integrity of the payout calculation.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/GenericBondCalculator.sol#L46

amountOLAS = ITokenomics(tokenomics).getLastIDF() * totalTokenValue / 1e36;

Implement a mechanism to verify the integrity of the data returned by the external contract. This could include sanity checks on the values returned, such as ensuring they are within expected ranges, or using a commit-reveal scheme to ensure the data has not been tampered with between updates.

Price Manipulation in getCurrentPriceLP function

The getCurrentPriceLP function calculates the price based on the reserves of the Uniswap pair. An attacker could temporarily manipulate the reserves by performing a large trade just before the price is fetched, leading to an incorrect price calculation.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/GenericBondCalculator.sol#L70

priceLP = (reserve1 * 1e18) / totalSupply;

To protect against this, consider using an oracle that provides time-weighted average prices (TWAP) to smooth out temporary price fluctuations. Additionally, implementing slippage protection or maximum price change limits between consecutive calls could help mitigate the impact of price manipulation.

Lack of Input Validation

The calculatePayoutOLAS function does not validate the priceLP and tokenAmount inputs, which could lead to unexpected behavior if invalid values are passed.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/GenericBondCalculator.sol#L46

function calculatePayoutOLAS(uint256 tokenAmount, uint256 priceLP) external view returns (uint256 amountOLAS)

Implement input validation checks to ensure that the values provided are within reasonable and expected ranges. This could include checks against historical data or predefined limits to prevent extreme values from being used in calculations.

Unchecked External Contract Interaction

The contract interacts with the IUniswapV2Pair interface without checking the success of the call or the validity of the returned data.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/GenericBondCalculator.sol#L80

(reserve0, reserve1, ) = pair.getReserves();

Mitigation: Ensure that the data returned from external contract calls is valid and that the calls themselves are successful. This could involve checking return values and using try/catch blocks to handle failed calls gracefully.

Timestamp Dependence

The checkpoint function uses block.timestamp to determine the end of an epoch. The reliance on block.timestamp can be manipulated by miners to a certain degree and might introduce timing vulnerabilities.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Tokenomics.sol

Consider using block numbers instead of timestamps for epoch calculations, as they are less prone to manipulation. If timestamps must be used, ensure there's a tolerance for the time drift that can occur.

Loop Over External Calls

In the trackServiceDonations function, there's a loop that makes external calls to IServiceRegistry.exists. This could lead to a denial of service if the loop grows too large or if the external service is unresponsive.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Tokenomics.sol#L788

Consider validating service existence outside of the loop, or ensure that there's an upper bound on the number of iterations to prevent excessive gas consumption and potential denial of service.

Inefficient Storage Access

The accountOwnerIncentives function reads from storage multiple times within a loop, which is gas-inefficient.

Cache storage variables in memory before the loop to reduce gas costs.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Tokenomics.sol#L1085

function accountOwnerIncentives(address account, uint256[] memory unitTypes, uint256[] memory unitIds) external returns (uint256 reward, uint256 topUp) { // ... (existing checks and setup) // Cache storage variables in memory uint256 curEpoch = epochCounter; IncentiveBalances memory incentiveBalance; for (uint256 i = 0; i < unitIds.length; ++i) { // Read once from storage incentiveBalance = mapUnitIncentives[unitTypes[i]][unitIds[i]]; // ... (use incentiveBalance instead of mapUnitIncentives for calculations) // Clear storage balances mapUnitIncentives[unitTypes[i]][unitIds[i]].reward = 0; mapUnitIncentives[unitTypes[i]][unitIds[i]].topUp = 0; } // ... (rest of the function) }

Timestamp Dependence

The checkpoint function uses block.timestamp to determine the end of an epoch. The reliance on block.timestamp can be manipulated by miners to a certain degree and might introduce timing vulnerabilities.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Tokenomics.sol

Consider using block numbers instead of timestamps for epoch calculations, as they are less prone to manipulation. If timestamps must be used, ensure there's a tolerance for the time drift that can occur.

Loop Over External Calls

In the trackServiceDonations function, there's a loop that makes external calls to IServiceRegistry.exists. This could lead to a denial of service if the loop grows too large or if the external service is unresponsive.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Tokenomics.sol#L788

Consider validating service existence outside of the loop, or ensure that there's an upper bound on the number of iterations to prevent excessive gas consumption and potential denial of service.

Inefficient Storage Access

The accountOwnerIncentives function reads from storage multiple times within a loop, which is gas-inefficient.

Cache storage variables in memory before the loop to reduce gas costs.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/tokenomics/contracts/Tokenomics.sol#L1085

function accountOwnerIncentives(address account, uint256[] memory unitTypes, uint256[] memory unitIds) external returns (uint256 reward, uint256 topUp) { // ... (existing checks and setup) // Cache storage variables in memory uint256 curEpoch = epochCounter; IncentiveBalances memory incentiveBalance; for (uint256 i = 0; i < unitIds.length; ++i) { // Read once from storage incentiveBalance = mapUnitIncentives[unitTypes[i]][unitIds[i]]; // ... (use incentiveBalance instead of mapUnitIncentives for calculations) // Clear storage balances mapUnitIncentives[unitTypes[i]][unitIds[i]].reward = 0; mapUnitIncentives[unitTypes[i]][unitIds[i]].topUp = 0; } // ... (rest of the function) }

Improper validation in depositTokenForOLAS function

The token allowance check happens after initiating the token transfer. This could allow the transfer to fail if allowance is insufficient. Better to validate allowance first.

Lines of code

if (IToken(token).allowance(account, address(this)) < tokenAmount) { revert InsufficientAllowance(...) }
bool success = IToken(token).transferFrom(...) if(!success) { revert TransferFailed(...) }

Lack of validation of tokenomics contract address

The rebalanceTreasury function does not validate that the tokenomics address is correctly set. This could allow it to be changed to a malicious contract.

Lines of code

Add check that msg.sender equals the stored tokenomics address variable.

The contract uses a low-level call for ETH transfers

Use transfer or send for ETH transfers to prevent reentrancy, or ensure all state changes occur before the call.

Lines of code

(success, ) = to.call{value: tokenAmount}("");

Change:

// Using transfer for safer ETH transfers
to.transfer(tokenAmount);

External Calls and Interface Risks

The contract makes external calls to other contracts, which could potentially fail or be manipulated if the called contracts are malicious.

Consider using try/catch for handling external calls that could fail.

Lines of code

ITokenomics(tokenomics).trackServiceDonations(msg.sender, serviceIds, amounts, msg.value);

Change:

try ITokenomics(tokenomics).trackServiceDonations(msg.sender, serviceIds, amounts, msg.value) { // Handle successful call } catch { // Handle failed call }

Access control issue in drainServiceSlashedFunds

This function allows the contract owner to drain funds from the ServiceRegistry. However, there is no check that the ServiceRegistry address is set correctly.

Lines of code

Add check that serviceRegistry address matches expected value:

if (serviceRegistry != ITokenomics(tokenomics).serviceRegistry()) { revert Unauthorized(); }

Unchecked return values

The contract assumes that all token transfers will succeed without explicitly checking the return value. Check the return value of the token transfer function to ensure it was successful.

Lines of code

bool success = IToken(token).transferFrom(account, address(this), tokenAmount); if (!success) { revert TransferFailed(token, account, address(this), tokenAmount); }

Change:

require(IToken(token).transferFrom(account, address(this), tokenAmount), "TransferFrom failed");

The ERC20 standard does not enforce that a transfer must revert on failure. Instead, it should return false. It's important to check the return value to ensure that the transfer was successful.

The receive function does not check if the contract is paused before accepting ETH.

Add a check to ensure that the contract is not paused before accepting ETH.

Lines of code

receive() external payable { // ... existing logic ... }

After:

receive() external payable whenNotPaused { // ... existing logic ... }

Adding a check for the paused state in the receive function prevents the contract from accepting ETH when it is not operational, which could be a part of the contract's safety mechanism.

Denial of Service

The withdraw function assumes that the firstAvailablePositionAccountIndex always points to an account with liquidity. This could lead to a denial of service if no liquidity is available at that index.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/lockbox-solana/solidity/liquidity_lockbox.sol

address positionAddress = positionAccounts[firstAvailablePositionAccountIndex];
address positionAddress; bool found = false; for (uint32 i = firstAvailablePositionAccountIndex; i < numPositionAccounts; ++i) { if (mapPositionAccountLiquidity[positionAccounts[i]] > 0) { positionAddress = positionAccounts[i]; found = true; break; } } require(found, "No available position with liquidity");

Iterating through the positionAccounts until a position with liquidity is found prevents the function from failing due to an incorrect index. This change ensures that the function does not revert unnecessarily, avoiding a potential denial of service.

The deposit function transfers tokens without checking the return value.

This could lead to a loss of funds if the transfer fails silently.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/lockbox-solana/solidity/liquidity_lockbox.sol

SplToken.transfer(...);
change to:
require( SplToken.transfer(...), "Token transfer failed" );

Checking the return value of the transfer function ensures that the transfer was successful. If the transfer fails, the transaction will revert, preventing any further execution and potential loss of funds.

The original code checks for equality against a single value, which is likely incorrect. The corrected code checks that the tick indices are within the allowed range, ensuring proper validation.

Check External Calls

The contract makes external calls without considering the possibility of failure. This could lead to inconsistent state if the external call fails but the contract state has already been updated.

https://github.com/code-423n4/2023-12-autonolas/blob/main/lockbox-solana/solidity/liquidity_lockbox.sol

Before:
whirlpool.decreaseLiquidity(...);
change to:
bool success = whirlpool.decreaseLiquidity(...); require(success, "External call to whirlpool failed");

Checking the success of the external call will ensure that the state of the contract only updates if the external call was successful. This prevents inconsistencies in the contract state.

Incorrect Tick Index Validation

https://github.com/code-423n4/2023-12-autonolas/blob/main/lockbox-solana/solidity/liquidity_lockbox.sol#L84C5-L97C10

The _getPositionData function checks for specific tick indices, which may not be appropriate for all positions. This could lead to valid positions being rejected.

Lines of code

if (positionData.tickLowerIndex != minTickLowerIndex || positionData.tickUpperIndex != maxTickLowerIndex) { revert("Wrong ticks"); }

Validate the tick indices against a range of acceptable values rather than specific ones. This allows for greater flexibility and prevents legitimate positions from being incorrectly invalidated.

Denial of Service in Withdraw Function

The withdraw function relies on the firstAvailablePositionAccountIndex to point to an account with liquidity. If this index points to an account without liquidity, the function will revert, potentially causing a denial of service for legitimate withdrawals.

Lines of code

address positionAddress = positionAccounts[firstAvailablePositionAccountIndex];

Implement a mechanism to skip over position accounts with zero liquidity. This could involve iterating through the positionAccounts array until an account with sufficient liquidity is found or restructuring how position accounts are tracked to avoid gaps.

Lack of Event Emission After Sensitive Actions

The contract does not emit events after sensitive actions such as deposits and withdrawals. This makes it difficult to track these actions off-chain and can be exploited by malicious actors to perform actions without being easily detected.

Lines of code

Emit events after state-changing actions to provide transparency and enable off-chain monitoring. For example, emit a Deposit event after a successful deposit and a Withdrawal event after a successful withdrawal.

Slippage Not Accounted for in Liquidity Withdrawals

The withdraw function does not account for slippage, which could result in users receiving fewer tokens than expected when removing liquidity.

Lines of code

whirlpool.decreaseLiquidity{accounts: metasDecreaseLiquidity, seeds: [[pdaProgramSeed, pdaBump]]}(amount, 0, 0);

Include slippage protection by specifying minimum amounts for the tokens to be received when decreasing liquidity. This can be done by calculating the minimum acceptable amounts based on the current pool prices and an acceptable slippage percentage.

Unchecked External Calls

The functions pda_mint_to, transfer, and pda_burn make external calls to the Solana token program without checking the success of these calls. If the external call fails, the Solidity contract will not revert, potentially leading to a state of inconsistency or silent failures.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/lockbox-solana/solidity/library/spl_token.sol

tokenProgramId.call{accounts: metas, seeds: [[seed, bump]]}(instr);

Check Return Values of External Calls To mitigate this issue, the contract should check the return value of the external call and revert if the call was unsuccessful. This ensures that the contract's state remains consistent and that failures are handled appropriately.

(bool success, ) = tokenProgramId.call{accounts: metas, seeds: [[seed, bump]]}(instr); require(success, "External call failed");

By adding the require statement, we ensure that the entire transaction is reverted if the external call does not succeed. This prevents attackers from exploiting failed calls to cause unintended effects in the contract's logic or state.

Lack of Access Control

The functions pda_mint_to and pda_burn are designed to mint and burn tokens, respectively. These are highly sensitive operations that should be restricted to authorized users only. However, the code does not implement any explicit access control mechanisms to restrict who can call these functions.

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/main/lockbox-solana/solidity/library/spl_token.sol

function pda_mint_to(address mint, address account, address authority, uint64 amount, bytes seed, bytes bump) internal { ... } function pda_burn(address account, address mint, address owner, uint64 amount, bytes seed, bytes bump) internal { ... }

Implement Access Control To mitigate this issue, the contract should implement a robust access control mechanism to ensure that only authorized entities can execute these functions. This can be achieved by using modifiers that check whether the caller has the appropriate role or permissions.

modifier onlyMintAuthority(address authority) { require(msg.sender == authority, "Caller is not the mint authority"); _; }

By adding a modifier onlyMintAuthority, we ensure that only the designated mint authority can mint or burn tokens, which is a critical access control measure to prevent unauthorized token manipulation.

function pda_mint_to(address mint, address account, address authority, uint64 amount, bytes seed, bytes bump) internal onlyMintAuthority(authority) { ... } function pda_burn(address account, address mint, address owner, uint64 amount, bytes seed, bytes bump) internal onlyMintAuthority(owner) { ... }

#0 - c4-pre-sort

2024-01-10T13:46:54Z

alex-ppg marked the issue as sufficient quality report

#1 - c4-judge

2024-01-19T22:05:34Z

dmvt marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter