code-423n4 / 2023-07-pooltogether-findings

12 stars 7 forks source link

Attacker can force users to delegate to `SPONSORSHIP_ADDRESS` #393

Closed code423n4 closed 11 months ago

code423n4 commented 1 year ago

Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L480-L482 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L494-L504 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L982-L994

Vulnerability details

Impact

An attacker can change the delegatee of a user who deposited into the vault to the SPONSORSHIP_ADDRESS address by calling one of the functions sponsor or sponsorWithPermit and giving the address of the user as _receiver.

The impact of this issue is that the attacker can stop users from winning prizes because the users who have delegated to the SPONSORSHIP_ADDRESS address get their chances of winning prizes revoked.

Proof of Concept

As the functions sponsor and sponsorWithPermit do the same thing except the later include a permit functionality which doesn't have an impact on this issue, i will explain how this issue can be exploited with the sponsor function and the same apply to the sponsorWithPermit function.

We start with the sponsor function :

function sponsor(uint256 _assets, address _receiver) external returns (uint256) {
    return _sponsor(_assets, _receiver);
}

As you can see the function can be called by anyone, and it takes the _assets amount of asset to deposit into the vault and a _receiver address that the user wants to deposit to, the function calls the internal function _sponsor below :

function _sponsor(uint256 _assets, address _receiver) internal returns (uint256) {
    uint256 _shares = deposit(_assets, _receiver);

    if (
      _twabController.delegateOf(address(this), _receiver) != _twabController.SPONSORSHIP_ADDRESS()
    ) {
      _twabController.sponsor(_receiver);
    }

    emit Sponsor(msg.sender, _receiver, _assets, _shares);

    return _shares;
}

Inside this function we can see that it call the deposit function for performing the deposit of the asset amount, and then the function checks the current delegatee of the _receiver address, if this one is different from the SPONSORSHIP_ADDRESS address the function will call the _twabController.sponsor function to changes the _receiver delegatee address to the SPONSORSHIP_ADDRESS address :

// TwabController.sponsor
function sponsor(address _from) external {
    _delegate(msg.sender, _from, SPONSORSHIP_ADDRESS);
}

In the comments of the TwabController contract we can see the following :

/// @notice Allows users to revoke their chances to win by delegating to the sponsorship address.
address public constant SPONSORSHIP_ADDRESS = address(1);

Basically any users who is delegating to the SPONSORSHIP_ADDRESS address is not included in the prize distribution and has no chances to win.

So to resume any attacker can change all other users delegations to the SPONSORSHIP_ADDRESS address and deprive them from the chance of winning prizes, and this attack will not cost the attacker any funds as he can at each time deposit a very small amount to every user when calling the sponsor function.

The scenario of the attack can be summarized as follows :

Alice could find out about this after a while and change the delegation but until that time she will be thinking that she's participating in the lottery when in fact she's not. And Bob can repeat the same attack on other users who also deposited into the Vault.

Tools Used

Manual review

Recommended Mitigation Steps

There are many ways to solve this issue but the simplest one is to add a check in the _sponsor function which changes the delegation only if : _receiver == msg.sender.

The function _sponsor should be updated as follows :

function _sponsor(uint256 _assets, address _receiver) internal returns (uint256) {
    uint256 _shares = deposit(_assets, _receiver);
    // @audit only change delegation when caller is same as _receiver
    if (
      msg.sender == _receiver &&
      _twabController.delegateOf(address(this), _receiver) != _twabController.SPONSORSHIP_ADDRESS()
    ) {
      _twabController.sponsor(_receiver);
    }

    emit Sponsor(msg.sender, _receiver, _assets, _shares);

    return _shares;
}

Assessed type

Other

c4-judge commented 1 year ago

Picodes marked the issue as primary issue

c4-sponsor commented 1 year ago

asselstine marked the issue as sponsor confirmed

Picodes commented 11 months ago

Keeping High severity, considering this is an instance of "loss of yield"

c4-judge commented 11 months ago

Picodes marked the issue as satisfactory

c4-judge commented 11 months ago

Picodes marked issue #408 as primary and marked this issue as a duplicate of 408

PierrickGT commented 11 months ago

I've removed the receiver param in the following PR: https://github.com/GenerationSoftware/pt-v5-vault/pull/19 This way, only the msg.sender can sponsor the Vault by depositing into it and delegating to the sponsorship address if it is not already the case. If the user wants to deposit on behalf of another user, he can still use the deposit function. Funds will then be delegated to any address set by the receiver.