PoolTogether TWAB Delegator contest - WatchPug's results

A protocol for no loss prize savings on Ethereum

General Information

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

PoolTogether

Findings Distribution

Researcher Performance

Rank: 4/22

Findings: 2

Award: $913.72

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: CertoraInc

Also found by: Dravee, Rhynorater, chunter, gzeon, jayjonah8, robee

Labels

bug
QA (Quality Assurance)
sponsor acknowledged

Awards

835.6633 USDC - $835.66

External Links

Lines of code

https://github.com/pooltogether/v4-twab-delegator/blob/21bb53b2ea54a248bbd1d3170dbadd3a0c83d874/contracts/Delegation.sol#L39-L46

Vulnerability details

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.

https://github.com/pooltogether/v4-twab-delegator/blob/21bb53b2ea54a248bbd1d3170dbadd3a0c83d874/contracts/Delegation.sol#L39-L46

  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.

Recommendation

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."

Findings Information

🌟 Selected for report: CertoraInc

Also found by: 0x1f8b, Dravee, IllIllI, Omik, Tomio, WatchPug, gzeon, hickuphh3, kenta, nascent, pedroais, rfa, robee, sorrynotsorry, ye0lde, z3s

Awards

78.0635 USDC - $78.06

Labels

bug
G (Gas Optimization)

External Links

[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.

[S] Move lockUntil from Delegation to TWABDelegator can save gas from external call

The lockUntil of a Delegation is currently stored on the Delegation contract and usually used by the TWABDelegator contract through an external call (_delegation.lockUntil()).

https://github.com/pooltogether/v4-twab-delegator/blob/21bb53b2ea54a248bbd1d3170dbadd3a0c83d874/contracts/TWABDelegator.sol#L624-L626

  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.

[M] Use custom error may save gas

https://github.com/pooltogether/v4-twab-delegator/blob/21bb53b2ea54a248bbd1d3170dbadd3a0c83d874/contracts/Delegation.sol#L30-L30

[M] ++i is more gas efficient than i++

https://github.com/pooltogether/v4-twab-delegator/blob/21bb53b2ea54a248bbd1d3170dbadd3a0c83d874/contracts/Delegation.sol#L42-L42

[N] "> 0" is less efficient than "!= 0" for unsigned integers

https://github.com/pooltogether/v4-twab-delegator/blob/21bb53b2ea54a248bbd1d3170dbadd3a0c83d874/contracts/TWABDelegator.sol#L601-L601

#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

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