echidna icon indicating copy to clipboard operation
echidna copied to clipboard

Feature request: Being able to do transactions on behalf of other addresses

Open rappie opened this issue 3 years ago • 2 comments

Use case

This use case is inspired by Damn Vulnerable DeFi - Unstoppable. https://www.damnvulnerabledefi.xyz/challenges/1.html

There are 3 users:

  • Admin. Deploys contracts, is owner, etc.
  • User. Uses the contracts, is the victim of the attack in this case
  • Attacker

In hardhat you would set this up using something like this to deploy the contracts and run a testcase.

[deployer, attacker, someUser] = await ethers.getSigners();
...
deploy_and_test();

After deployment, recording with Etheno and using a simple E2E testing contract you want to fuzz this with Echidna as attacker. This can be done by using the attacker address in the sender list in Echidna's config file.

The test Echidna does has to be done as the victim (using the victim's from address). This is nessecary to be able to detect if the attack was succesful because msg.sender is used in the contract to check balances etc. The problem here is that all tests are performed using the E2E contract, so the from address will be the one of the contract and not the victim.

Workaround

As a hacky fix I was able to use the contractAddr setting to change the address of the E2E contract to someUser's address. Using this i was able to succesfully break the DVDeFi Unstoppable challenge.

I was being told that using contractAddr this way is not officially supported. Also, you can only do your transactions on behalf of 1 address. Some more complicated use cases may have multiple users interacting.

There is a known workaround for this problem using proxy Contracts. However, either I don't fully understand this workaround or it falls short in these kinds of use cases. Please let me know :)

When you use a proxy contract you will be able to do transactions from multiple addresses. You create a contract besides the E2E contract and make calls to it to perform checks or do fuzzing.

The challenge here is that the address of this proxy contract needs to match the address of someUser. I see a couple of ways to achieve this:

  • Deploy the proxy contract with the hardhat deployment script and edit the address in the Etheno log to match someUser
  • Stop using someUser alltogether in both the deployment script and Echidna and only use the contract to do the actions
  • Some kind of multisig situation? (not sure)

All these solutions do have their downsides. There is a lot of manual tweaking and you need to make (big) changes to the deployment scripts.

Proposed solution

First of all: I don't have enough knowledge of the inner workings of Echidna to know the best solution. These are just suggestions.

In my opinion the essence of the problem is that Echidna is limited to only using its "own" contract to do actions and checks. This way it is always a bystander and not a full participant.

As a solution we could create support to do transactions on behalf of other addresses. This has is to be communicated to Echidna in some shape or form. Some suggestions:

Decorators:

/**
* @echidna from <address>
*/
function echidna_test_something() public returns (bool) {
    return target.testSomething();
}

Argument with reserved name (echidna_from)

function echidna_test_something() public returns (bool) {
	address user_address = <address>
    return test_something_as_user(user_address);
}

function test_something_as_user(address echidna_from) public returns (bool) {
	return target.testSomething(); // this is called as from address in "echidna_from";
}

What this will do is let Echidna use the code in the E2E contract to perform a transaction but it will send this transaction to the EVM as if it were made from the address supplied by us.

There are probably tons of other options. Personally I like the decorators, as it would easily allow for more options in the future.

Let me know what you think. Thanks.

rappie avatar Apr 19 '22 11:04 rappie

Hey @rappie I'm not going to discuss too much about your proposed solutions but wanted to provide a few of my thoughts:

  1. I would recommend checking out this solution to the Unstoppable challenge. I think the key difference between the way you did it and the way I did it in the PR is that I blurred the lines between who is the attacker and who is the victim. I would argue that, at least in this instance, it does not matter. What you want Echidna to "figure out" is that if someone transfers directly to the lender then no one can take out a flash loan. So, I wrote a property that tries to always take out a flash loan and that's it. However, if Echidna (who is msg.sender) first calls transfer and then calls flashLoan, the property will fail. With Echidna I have found that is better to think less in the context of "attacker" / "victim" and more generally consider what is the property / invariant you are trying to test - in this case the property is: one should always be able to take out a flash loan, provided there is enough liquidity in the pool.
  2. I am actually a fan of the Proxy idea and we used it on a recent audit. I think you can mimic an "attacker" / "victim" with this method. Here is some code for inspiration:
contract Account {
    function proxy(address target, bytes memory _calldata)
        public
        returns (bytes memory)
    {
        (bool success, bytes memory returnData) = address(target).call(
            _calldata
        );
        require(success);
        return returnData;
    }
}

The above allows you to basically make any function call through a random Account. Now, in the constructor of your Echidna contract or in a separate init function you can set up two accounts (or as many as you want):

Account attacker = new Account();
Account victim = new Account();

Now, you can test Unstoppable in the following way:

bytes memory returnData = attacker.proxy(address(damnValuableToken), abi.encodeWithSelector(DamnValuableToken.transfer.selector, address(unstoppableLender), 10));
bytes memory returnData = victim.proxy(address(unstoppableLender), abi.encodeWithSelector(UnstoppableLender.flashLoan.selector, any_amount));
// decode returnData (if there is any) as you wish

Note that I just came up with this code on the fly and may require some re-work :)

Hope this provides you more insight into using Proxys as a way to emulate multiple senders! 3. If you don't like using the Proxy method, you can also use the Echidna contract as a conduit for the senders specified in the config file. The issue with this method is that token transfers can be a bit tricky because of token approvals. I can provide more guidance on this if you would like.

I'll leave it to others on the team to discuss your specific FR :)

anishnaik avatar Apr 19 '22 18:04 anishnaik

Thank you for the extensive response.

My approach to fuzzing so far has been to do as little rewrites as possible to the existing code and project structure. I personally believe this approach to be very powerful, especially if projects get bigger than just 1 or 2 contracts.

Right now I don't have the experience yet to show other concrete use cases. I'm currently working on both DVDeFi and bug bounty programs though. When I have more I will share it here.

rappie avatar Apr 28 '22 11:04 rappie

Should we close this in favor of prank support?

gustavo-grieco avatar Jan 12 '23 19:01 gustavo-grieco