Platform: Code4rena
Start Date: 22/02/2022
Pot Size: $30,000 USDC
Total HM: 1
Participants: 22
Period: 3 days
Judge: leastwood
Id: 93
League: ETH
Rank: 4/22
Findings: 2
Award: $913.72
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: CertoraInc
Also found by: Dravee, Rhynorater, chunter, gzeon, jayjonah8, robee
835.6633 USDC - $835.66
Delegation
is a contract deployed dedicated to holding the ticket
tokens for the delegator
and they can then be delegate
to a delegatee
.
On the Delegation
contract, there is a method named executeCalls()
designed for "Executes calls on behalf of this contract" which allows arbitrary code execution for the owner.
However, we found that the owner of Delegation
will always be TWABDelegator
, and the TWABDelegator
will only use Delegation.sol#executeCalls()
to call one particular address: the ticket
address, and for only two methods: transfer()
and delegate()
.
Furthermore, even though in Delegation.sol#executeCalls()
, calls[i].value
is used, the function is not being marked as payable
, that makes it hard for calls that requires eth payments.
function executeCalls(Call[] calldata calls) external onlyOwner returns (bytes[] memory) { bytes[] memory response = new bytes[](calls.length); for (uint256 i = 0; i < calls.length; i++) { response[i] = _executeCall(calls[i].to, calls[i].value, calls[i].data); } return response; }
While the ticket
is being delegated through TWABDelegator
, they won't be able to retrieve the tickets back until the lockUntil
, without the ability to make arbitrary code execution, the delegator
may miss some of the potential benefits as a holder of the ticket
tokens, for example, an airdrop to all holders of the ticket
tokens, or an NFT made mintable only for certain ticket holders.
Consider adding a new method on TWABDelegator
:
function executeCalls( address _delegator, uint256 _slot, Delegation.Call[] memory calls ) external payable returns (bytes[] memory) { _requireDelegatorOrRepresentative(_delegator); Delegation _delegation = Delegation(_computeAddress(_delegator, _slot)); if (block.timestamp < _delegation.lockUntil()) { for (uint256 i = 0; i < calls.length; i++) { if (calls[i].to == address(ticket)) { revert("TWABDelegator/delegation-locked"); } } } return _delegation.executeCalls{value: msg.value}(_calls); }
And also, consider making Delegation.sol#executeCalls()
a payable
method.
#0 - PierrickGT
2022-03-03T16:37:51Z
The issue outlined by the warden is relevant but users won't need to execute arbitrary calls cause potential rewards given out to ticket holders will be handled by our TWABRewards contract.
This contract retrieves users TWAB (Time-Weighted Average Balance) for a given period of time and calculate the amount of rewards they are eligible to.
Users can then claimRewards
on behalf of others. So delegatees will be able to claim their rewards and delegators could claim on their behalf.
https://github.com/pooltogether/v4-periphery/blob/348d2bf7cfcf5750bad4aae63b8ade5a2a45f188/contracts/TwabRewards.sol#L410 https://github.com/pooltogether/v4-periphery/blob/348d2bf7cfcf5750bad4aae63b8ade5a2a45f188/contracts/interfaces/ITwabRewards.sol#L94
For more informations about how the TWAB works, here is some documentation:
Also, by restricting calls to the transfer
and delegate` methods on the ticket, we limit the attack surface and any attack vector we may not have thought about.
For the reasons above, I've acknowledged the issue but we won't implement the proposed solution
#1 - 0xleastwood
2022-03-09T09:09:10Z
I don't really see a case where _delegateCall()
or _transferCall()
will need to have some ETH attached with it. They are solely dealing with the Ticket ERC20 token and updating delegation data. Considering the fact that rewards are handled by a separate contract, I think its fair to downgrade this to 1 (Low Risk)
.
#2 - CloudEllie
2022-03-25T03:16:18Z
Since this issue was downgraded to a QA level, and the warden did not submit a separate QA report, we've renamed this one to "QA report" for consistency.
The original title, for the record, was "[WP-M1] delegator and/or representative should be allowed for arbitrary code execution besides restricted operations during unlocked period."
[S]: Suggested optimation, save a decent amount of gas without compromising readability;
[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;
[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.
lockUntil
from Delegation
to TWABDelegator
can save gas from external callThe lockUntil
of a Delegation
is currently stored on the Delegation
contract and usually used by the TWABDelegator
contract through an external call (_delegation.lockUntil()
).
function _requireDelegationUnlocked(Delegation _delegation) internal view { require(block.timestamp >= _delegation.lockUntil(), "TWABDelegator/delegation-locked"); }
In comparison to storing the lockUntil
on TWABDelegator
contract, this creates an overhead of one external call.
#0 - PierrickGT
2022-03-01T22:22:31Z
[S] Move lockUntil from Delegation to TWABDelegator can save gas from external call
The lockUntil
value is specific to each delegation so it can't live in the TWABDelegator contract.
[M] Use custom error may save gas
Duplicate of https://github.com/code-423n4/2022-02-pooltogether-findings/issues/30
For the other issues, duplicate of https://github.com/code-423n4/2022-02-pooltogether-findings/issues/15