Llama - Toshii's results

A governance system for onchain organizations.

General Information

Platform: Code4rena

Start Date: 06/06/2023

Pot Size: $60,500 USDC

Total HM: 5

Participants: 50

Period: 8 days

Judge: gzeon

Id: 246

League: ETH

Llama

Findings Distribution

Researcher Performance

Rank: 5/50

Findings: 3

Award: $3,075.56

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: auditor0517

Also found by: 0xnev, T1MOH, Toshii, kutugu

Labels

bug
3 (High Risk)
satisfactory
duplicate-203

Awards

2574.2156 USDC - $2,574.22

External Links

Lines of code

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L199-L210 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L604-L607 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L568-L570 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L282-L285

Vulnerability details

Impact

The LlamaRelativeQuorum contract is intended to allow for approvals/disapprovals to be configured such that, for example, an approval is triggered when a certain percentage of the total quantity for a role (with this quantity being fixed at the creation of the action) is approved. The issue is however, that instead of using the total quantity, as the functionality intended for, the validateActionCreation function instead calls getRoleSupplyAsNumberOfHolders on the policy contract to set the denominator approvalPolicySupply for this equation. This means they are getting the number of holders rather than the total quantity, whereas each holder can have >= 1 quantity.

This results in the percentage being invalid, as the calculation will use the incorrect denominator. This completely breaks the approval/disapproval functionality of this strategy. In most cases this will result in approvals/disapprovals being granted with significantly less than expected votes in favor of those actions.

Proof of Concept

Let's consider the example where we are creating an action and users are voting in favor of it. In the validateActionCreation function, the total supply used to check for quorum is gathered as follows:

function validateActionCreation(ActionInfo calldata actionInfo) external {
	LlamaPolicy llamaPolicy = policy; // Reduce SLOADs.
	uint256 approvalPolicySupply = llamaPolicy.getRoleSupplyAsNumberOfHolders(approvalRole);
	if (approvalPolicySupply == 0) revert RoleHasZeroSupply(approvalRole);
	...
	actionApprovalSupply[actionInfo.id] = approvalPolicySupply;
}

approvalPolicySupply is defined as being the output of calling getRoleSupplyAsNumberOfHolders rather than, what it should be, the output from calling getRoleSupplyAsQuantitySum. This becomes an issue when we consider the implementation for isActionApproved:

function isActionApproved(ActionInfo calldata actionInfo) public view returns (bool) {
	Action memory action = llamaCore.getAction(actionInfo.id);
    return action.totalApprovals >= _getMinimumAmountNeeded(actionApprovalSupply[actionInfo.id], minApprovalPct);
}

it calls _getMinimumAmountNeeded:

function _getMinimumAmountNeeded(uint256 supply, uint256 minPct) internal pure returns (uint256) {
    // Rounding Up
    return FixedPointMathLib.mulDivUp(supply, minPct, ONE_HUNDRED_IN_BPS);
}

action.totalApprovals is the summation of quantities of the users who approved, and here is being compared against some percentage of the number of users who have this role. This is an invalid comparison and doesn't actually capture the percentage of approvals, resulting in this function and isActionDisapproved being invalid and gameable.

Tools Used

Manual review

In the validateActionCreation function of the LlamaRelativeQuorum contract, update all calls of llamaPolicy.getRoleSupplyAsNumberOfHolders(..) to llamaPolicy.getRoleSupplyAsQuantitySum(..)

Assessed type

Other

#0 - c4-pre-sort

2023-06-19T11:51:14Z

0xSorryNotSorry marked the issue as duplicate of #203

#1 - c4-judge

2023-07-06T13:51:45Z

gzeon-c4 marked the issue as satisfactory

Findings Information

Awards

54.5276 USDC - $54.53

Labels

bug
2 (Med Risk)
satisfactory
duplicate-247

External Links

Lines of code

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L317-L343 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaExecutor.sol#L29-L35

Vulnerability details

Impact

The execute function of the LlamaExecutor contract has a parameter value which allows you to specify an amount of ETH to send when calling a function on a target contract. The issue is that when calling the executeAction function of the LlamaCore contract, which subsequently calls executor.execute(..), the ETH is not passed on to the executor contract. This means there's no ETH in the executor contract to make the appropriate function call, which can cause that action to always revert. Alternatively, if the function call still succeeds, msg.value will now be trapped in the LlamaCore contract, with there being no way to retrieve it.

Proof of Concept

Consider the implementation for executeAction of LlamaCore. It checks that msg.value == actionInfo.value, and actionInfo.value is passed to executor.execute(..), but msg.value remains in the LlamaCore contract:

function executeAction(ActionInfo calldata actionInfo) external payable {
	// Initial checks that action is ready to execute.
	Action storage action = actions[actionInfo.id];
	ActionState currentState = getActionState(actionInfo);

	if (currentState != ActionState.Queued) revert InvalidActionState(currentState);
	if (block.timestamp < action.minExecutionTime) revert MinExecutionTimeNotReached();
	if (msg.value != actionInfo.value) revert IncorrectMsgValue();

	action.executed = true;
	...
	
	// Execute action.
	(bool success, bytes memory result) =
	  executor.execute(actionInfo.target, actionInfo.value, action.isScript, actionInfo.data);

	if (!success) revert FailedActionExecution(result);
	...
}

in the executor.execute(..) call , the LlamaExecutor contract will natively try to call the function of the target contract using actionInfo.value, but this value is never explicitly provided to the executor contract:

function execute(address target, uint256 value, bool isScript, bytes calldata data)
	external
	returns (bool success, bytes memory result)
{
	if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore();
	(success, result) = isScript ? target.delegatecall(data) : target.call{value: value}(data);
}

Therefore this call will in most cases always revert. However if it ever succeeds, msg.value worth of ETH will then be trapped in the LlamaCore contract.

Tools Used

Manual review

Add a receive function in the LlamaExecutor contract which prior to the call of executor.execute(..) in the executeAction function, msg.value will be sent to the LlamaExecutor contract

Assessed type

ETH-Transfer

#0 - c4-pre-sort

2023-06-19T11:11:13Z

0xSorryNotSorry marked the issue as duplicate of #255

#1 - c4-pre-sort

2023-06-19T11:13:11Z

0xSorryNotSorry marked the issue as not a duplicate

#2 - c4-pre-sort

2023-06-19T11:15:29Z

0xSorryNotSorry marked the issue as duplicate of #247

#3 - c4-judge

2023-07-02T10:30:27Z

gzeon-c4 marked the issue as satisfactory

Findings Information

🌟 Selected for report: ktg

Also found by: 0xnev, Atree, BLOS, Toshii, auditor0517, xuwinnie

Labels

bug
2 (Med Risk)
satisfactory
duplicate-64

Awards

446.8103 USDC - $446.81

External Links

Lines of code

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L431-L433 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L404-L409 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L478-L480

Vulnerability details

Impact

The _assertNoActionCreationsAtCurrentTimestamp function of the LlamaPolicy contract is intended to prevent the situation in which the role associated with an action has a change in total supply (calling _setRoleHolder) in the same block as when the action was created. This function is checked at the beginning of all calls to _setRoleHolder, wherein _setRoleHolder is the function which controls all changes to role assignments, both removing roles from users and adding roles to users.

There is an issue with this check however, in that any malicious user which has any trivial role allowing them the ability to create any action can DOS all attempts to add/remove/change roles, as they can frontrun all calls to _setRoleHolder with a call to create an action. This is especially simple for Ethereum. The malicious user can then arbitrarily affect the assignment of roles to their own benefit.

Proof of Concept

The _setRoleHolder function of the LlamaPolicy contract is used to control all role assignments for users, both removing and adding roles. Each call to _setRoleHolder first checks _assertNoActionCreationsAtCurrentTimestamp:

function _setRoleHolder(uint8 role, address policyholder, uint128 quantity, uint64 expiration) internal {
	_assertNoActionCreationsAtCurrentTimestamp();
	_assertValidRoleHolderUpdate(role, quantity, expiration);
	...
}

_assertNoActionCreationsAtCurrentTimestamp is intended to prevent a change to role supply in the same block as when an action related to that role is created. It is defined as follows:

function _assertNoActionCreationsAtCurrentTimestamp() internal view {
    if (llamaExecutor == address(0)) return; // Skip check during initialization.
    address llamaCore = LlamaExecutor(llamaExecutor).LLAMA_CORE();
    uint256 lastActionCreation = LlamaCore(llamaCore).getLastActionTimestamp();
    if (lastActionCreation == block.timestamp) revert ActionCreationAtSameTimestamp();
}

the (lastActionCreation == block.timestamp) condition allows for a malicious user with any trivial role which allows for any action creation (even if the action is completely unrelated to the role being updated) to frontrun any call to the _setRoleHolder function, which will cause this check to revert. This is because LlamaCore(llamaCore).getLastActionTimestamp() will reference the malicious user's created action. To perform this attack, the malicious user will monitor the mempool and frontrun any call to _setRoleHolder which doesn't benefit them with a call to create a new action (per block).

Tools Used

Manual review

At a minimum, _assertNoActionCreationsAtCurrentTimestamp should only check whether, for that block.timestamp, the approvalRole(s) of the created actions are the same as the role which is meant to be assigned/removed/updated in _setRoleHolder. This can be done by adding an additional mapping of the following form: (timestamp => role => bool), which is updated in the _createAction function of the LlamaCore contract. This mapping can then be referenced in the _assertNoActionCreationsAtCurrentTimestamp function to better determine if an action related to the role to be changed has been created during that timestamp.

Assessed type

DoS

#0 - c4-pre-sort

2023-06-19T12:07:26Z

0xSorryNotSorry marked the issue as duplicate of #10

#1 - c4-judge

2023-07-02T11:08:01Z

gzeon-c4 marked the issue as duplicate of #209

#2 - c4-judge

2023-07-02T11:14:19Z

gzeon-c4 marked the issue as satisfactory

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