aragon / aragonOS

(Aragon 1) Reference implementation for aragonOS: a Solidity framework for building complex dApps and protocols
https://hack.aragon.org/docs/aragonos-intro.html
GNU General Public License v3.0
686 stars 245 forks source link

Implement token whitelist for token transactions #72

Closed izqui closed 7 years ago

izqui commented 7 years ago

Tokens are arbitrary contracts that can run malicious code. Some sort of whitelist has to be implemented for the tokens that are allowed to execute transactions in the DAO.

There is an attack possible where you can create a fake token contract and make transactions to the DAO as another person. Only tokens with trusted code (that doesn't allow impersonation) should be added to this whitelist.

I think a good solution for this would be to implement a token whitelist in BylawsApp, and return false in canPerformAction if the token that performed the action is not in the whitelist (and therefore the sender cannot be trusted)

JGcarv commented 7 years ago

Hello @izqui , I would like to tackle this one! Although the solution itself is quite simple, I'm a bit afraid to put things in the wrong place, given the somewhat complex architecture.

My main question is, should I implement a whitelistToken(address token) function in the bylaw app itself? Because it seems a bit off place, as the bylawas app mostly defines the types of ByLaws and how to resolve them.

And related to this, the whitelistToken(address token) function itself should be restricted, or it wouldn't make a difference because the malicious token could call it, and therefore should be govern by a bylaw policy, but which one? Or should be open by default and linked to a specific kind later on?

Thanks in advance for the help!

izqui commented 7 years ago

Hello @JGcarv, sorry for the delay.

I think it would make sense to make it as a 'global setting' to all bylaws, an automatically reject actions in which the token is not in the whitelist.

The whitelistToken(...) function should definitely be protected with a onlyDAO modifier so it gets protected with bylaws (how meta ;))

JGcarv commented 7 years ago

Thanks for the responde @izqui!

Just a few more question: What should be the behavior of tokenized eth? Allowed by default?

Also, when dealing with ether, canPerformAction is always called with 0 for token or it sometimes uses EtherToken address?

izqui commented 7 years ago

That's a very good question.

There are two different cases:

Even though the EtherToken should be explicitly whitelisted if trusted to do token transfers, it could be whitelisted by default configuration.

izqui commented 7 years ago

A problem with the whitelist is that it will block all token deposits from not whitelisted tokens (which I don't think it is the expected behavior).

The problem solved with this is not allowing impersonations because of using untrusted tokens, so there is no problem to allow transactions that don't execute anything.

This would require changing the Kernel dispatch function checks order like this:

Current:

function dispatch(address _sender, address _token, uint256 _value, bytes _payload) internal {
        require(canPerformAction(_sender, _token, _value, _payload));
        vaultDeposit(_token, _value); // deposit tokens that come with the call in the vault
        if (_payload.length == 0) return; // Just receive the tokens

New:

function dispatch(address _sender, address _token, uint256 _value, bytes _payload) internal {
        vaultDeposit(_token, _value); // deposit tokens that come with the call in the vault
        if (_payload.length == 0) return; // Just receive the tokens
        require(canPerformAction(_sender, _token, _value, _payload));
izqui commented 7 years ago

Also receiveApproval(address _sender, uint256 _value, address _token, bytes _data) method is sometimes used even if the token is not calling it (see we use the _token parameter as the token).

This allows for trustless deposits of tokens (while being able to account for it). The problem is that the _sender parameter and _data can only be trusted that it came from the token in the case that _token == msg.sender.

I would modify the dispatch call in receiveApproval to: dispatch(_token == msg.sender ? _sender : msg.sender, _token, _value, _token == msg.sender ? _data : new bytes(0));

JGcarv commented 7 years ago

Thanks for the answer! I'm already testing the modifications, but I'm seriously stuck here, for some reason, I cannot make it work.

Now, reading the code, I stumbled across a comment in VaultOrgan.sol that says:

@dev deposit is not reachable on purpose using normal dispatch route

What does that mean? It should be reachable, since the dispatch in Kernel.sol calls vaultDeposit, which makes the deposit correct? Or I am missing something? Can this be the reason the deposits are not working on the tests?

JGcarv commented 7 years ago

I've investigated a bit, and I feel like there's a problem with the deposit function in the VaultOrgan. Look:

function deposit(address _token, uint256 _amount)
    check_blacklist(_token)
    external
    payable
    {
        if (_amount == 0)
            return;
        if (_token == 0 && msg.value == _amount)
            tokenizeEther(_amount); // if call has ETH, we tokenize it

        address token = _token == 0 ? getEtherToken() : _token;

        uint256 currentBalance = getTokenBalance(token);
        // This will actually be dispatched every time balance goes from 0 to non-zero.
        // The idea is that the frontend can listen for this event in all DAO history.
        // TODO: Is an event for when a certain token balance goes to 0 needed?
        if (currentBalance == 0)
            NewTokenDeposit(token);

        // TODO: Aragon Network funds redirect goes here :)

        uint256 newBalance = safeAdd(currentBalance, _amount); // - aragonNetworkFee;
        // Check token balance isn't less than expected.
        // Could be less because of a faulty erc20 implementation (can't trust)
        // Could be more because a token transfer can be done without notifying
        assert(newBalance <= ERC20(token).balanceOf(this));

        setTokenBalance(token, newBalance);
        Deposit(token, dao_msg().sender, _amount);
    }

If the token in question is an ERC223 token, it implements the transfer function that calls tokenFallback, which is implemented in the Kernel and calls the deposit function above.

Now , in the assert(newBalance <= ERC20(token).balanceOf(this)); line the token transfer transaction hasn't yet finished, so the balanceOf(this) is still a lower value than the newBalance making the whole transaction fail.

izqui commented 7 years ago

Most ERC223 implementations (including the one in test/helpers) change the token balance before making the call to the receiver, therefore even though the call hasn't 'finished' if the receiver were to make a check of the balance, it should already be updated.

Also why do you say deposits are not working on the tests? All tests from master are passing AFAIK, including the 'test/dao_dispatch.js' tests where erc223 is tested

JGcarv commented 7 years ago

Damn, that makes sense. Now I don't really know what I was thinking before to come to that conclusion.

Thanks for the insight

izqui commented 7 years ago

Did you solve it? If you published a draft PR i could try to help finding the issue!

izqui commented 7 years ago

Hi @JGcarv! How is progress going? i'd love to get this merged this week

JGcarv commented 7 years ago

Hey! I haven't done any progress, to be honest.

Somehow testing this contract is pretty frustrating, as nothing behaves as I expected(And I tried many variations).

I'll make a PR in a minute with my current state and hopefully you can shed some light on this.