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
Rank: 19/50
Findings: 3
Award: $128.38
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: libratus
Also found by: 0xcm, BRONZEDISC, Co0nan, Go-Langer, Madalad, MiniGlome, QiuhaoLi, T1MOH, Toshii, Udsen, ernestognw, flacko, joestakey, minhquanym, n1punp, rvierdiiev, sces60107
54.5276 USDC - $54.53
https://github.com/code-423n4/2023-06-llama/blob/aac904d31639c1b4b4e97f1c76b9c0f40b8e5cee/src/LlamaExecutor.sol#L29 https://github.com/code-423n4/2023-06-llama/blob/aac904d31639c1b4b4e97f1c76b9c0f40b8e5cee/src/LlamaCore.sol#L334
Llama
instances have a separate LlamaExecutor
contract for action execution.
When calling LlamaCore.executeAction()
, the flow is the following (for simplicity, we ignore action guards):
Queued
, that the minimum execution time has been reached, and that the msg.value
passed in the call is the same as actionInfo.value
action.executed
is written as true
LlamaExecutor.execute()
is calledFile: LlamaCore.sol 333: (bool success, bytes memory result) = 334: executor.execute(actionInfo.target, actionInfo.value, action.isScript, actionInfo.data);
target
contract with the specified data
and value
.File: LlamaExecutor.sol 29: function execute(address target, uint256 value, bool isScript, bytes calldata data) 30: external 31: returns (bool success, bytes memory result) 32: { 33: if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); 34: (success, result) = isScript ? target.delegatecall(data) : target.call{value: value}(data); 35: }
The issue is that when calling LlamaExecutor.execute()
in LlamaCore.executeAction()
, the msg.value
is not passed.
Any action with a non-zero actionInfo.value
will not be able to be executed: as the msg.value
is not passed to LlamaExecutor
, the call will revert in execute
line 34.
This greatly limits the use of Llama
instances (restricting their actions to calls that cannot pass any msg.value
).
Note that it is not even possible to send the appropriate value needed to LlamaExecutor
before execution because the contract does not have any payable
method (though technically there are ways for the contract to receive ETH, either via selfdestruction
of another contract or by using a PoS validator and selecting the Executor
as recipient of issued rewards)
Either way, sending ETH to the Executor
separately would result in the loss of the msg.value
sent in LlamaCore.executeAction()
anyway, which would remain stuck in LlamaCore
.
Add this test to LlamaCore.t.sol
, showing that the execution fails when sending the correct msg.value
.
After amending the code as per the mitigation steps given below, the test should give a successful execution - which you can verifying by replacing the assertFalse(status)
with assertTrue(status)
function testAudit_Exec_Value() public { bytes memory data = abi.encodeCall(MockProtocol.receiveEth, ()); vm.prank(actionCreatorAaron); uint256 actionId = mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(mockProtocol), 1 ether, data, ""); ActionInfo memory _actionInfo = ActionInfo( actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(mockProtocol), 1 ether, data ); vm.warp(block.timestamp + 1); _approveAction(approverAdam, _actionInfo); _approveAction(approverAlicia, _actionInfo); vm.warp(block.timestamp + 6 days); mpCore.queueAction(_actionInfo); vm.warp(block.timestamp + 5 days); vm.deal(actionCreatorAaron, 1 ether); vm.prank(actionCreatorAaron); (bool status, bytes memory _data) = address(mpCore).call{value: 1 ether}((abi.encodeCall(mpCore.executeAction, (_actionInfo)))); //@audit: call failed even though we submitted the correct msg.value assertFalse(status); }
Manual Analysis
The handling of msg.value
must be done by setting LlamaExecutor.execute
as payable, and ensuring the call made in LlamaCore.executeAction()
sends the value
in the call.
File: LlamaCore.sol 333: (bool success, bytes memory result) = -334: executor.execute(actionInfo.target, actionInfo.value, action.isScript, actionInfo.data); +334: executor.execute{value: msg.value}(actionInfo.target, actionInfo.value, action.isScript, actionInfo.data);
File: LlamaExecutor.sol 29: function execute(address target, uint256 value, bool isScript, bytes calldata data) -30: external +30: external payable 31: returns (bool success, bytes memory result) 32: { 33: if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); 34: (success, result) = isScript ? target.delegatecall(data) : target.call{value: value}(data); 35: }
Payable
#0 - 0xSorryNotSorry
2023-06-18T17:28:28Z
The issue is well demonstrated, properly formatted, contains a coded POC. Marking as HQ.
#1 - c4-pre-sort
2023-06-18T17:28:33Z
0xSorryNotSorry marked the issue as high quality report
#2 - c4-pre-sort
2023-06-19T11:08:48Z
0xSorryNotSorry marked the issue as primary issue
#3 - c4-pre-sort
2023-06-19T11:10:48Z
0xSorryNotSorry marked the issue as duplicate of #255
#4 - c4-pre-sort
2023-06-19T11:14:24Z
0xSorryNotSorry marked the issue as not a duplicate
#5 - c4-pre-sort
2023-06-19T11:14:31Z
0xSorryNotSorry marked the issue as duplicate of #247
#6 - c4-judge
2023-07-02T10:31:31Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: 0xnev
Also found by: 0xSmartContract, K42, QiuhaoLi, VictoryGod, dirk_y, joestakey, ktg, kutugu, libratus, mahdirostami, neko_nyaa, peanuts, xuwinnie
Llama introduces a complex yet elegant infrastructure to give more granularity to on-chain organizations.
I evaluated the codebase following the architecture diagram given in the docs:
You will find below an overview of my analysis method, what risks can arise from the design decisions and whether these were tackled appropriately, and high-level conclusions of the potential problems encountered - which are detailed in the vulnerability reports sent.
I reviewed parameters used in deployments, focusing on the validation done to ensure no deployments could be made with an erroneous configuration.
The BOOTSTRAP_ROLE
ensures a safety net is always present in a Llama instance. While it does not guarantee fully functioning configuration, it is sufficient in the context of the factory, which is governed by the protocol.
The handling of custom URI in PolicyMetadata
requires care to be taken in the string handling, as problems can arise from malformed data strings in front end tools. More specifically, the "arbitrary" data in string concatenation could use more validation, as it can potentially lead to errors in off-chain tools querying those view functions.
The core contract was separated from the policy logic, the execution logic, and the strategy logic.
This modular approach not only makes the codebase more readable, but also adds a layer of security - especially on the executor side, preventing modifying the core instance during arbitrary action executions, ensuring safe use of delegatecalls. On the other hand, care needs to be taken to ensure the flow of data and value through the different "blocks" (ie contracts) is correct. In this modular instance, there is different storage in each part (policy, core, strategy). This requires care to be taken in storage updates, to ensure each storage update is in "sync" with the others.
The review was conducted to ensure:
All the above points were found to be appropriately handled, having enough sanitization and safeguards to ensure a smooth and safe action flow.
More attention was then taken onto the LlamaExecutor
- as it seems to have been amended following the latest audit.
A few flaws were found regarding the native token handling in the execution flow (more details in the dedicated reports)
As this contract is a template for the governed contracts that will perform execution logic, the focus was on the functions handling token approvals and transfers, ensuring in particular the soundness of function arguments.
No issues were found in this area.
12 hours
#0 - c4-judge
2023-07-02T15:52:56Z
gzeon-c4 marked the issue as grade-b