-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cheatcodes
): add vm.delegatePrank
to execute a delegatecall
in the context of the pranked address
#824
Comments
This current behavior should probably be the expected behavior, right? If you call You could workaround this by having the cheatcode throw an error if the next call after |
So actually the usecase I had in mind was for mocking the behavior of a smart contract on a mainnet fork. I was recently working on the payloads for a governance proposal on Aave, and was coming up with a testing plan. The obvious choice was to spin up a mainnet fork, prank an account that has a lot of AAVE, and go through the normal process of making a proposal, voting on it, and warping to a time where I can execute it. However, I wanted a bit of a simpler process, and since my proposal was simply going to have the Aave governance smart contract delegatecall into a custom "payload" contract which then handles all the proposal logic (this is a common pattern in on-chain governance). I was hoping to prank into the aave governance contract address, and delegatecall into my payload contract myself so I didn't have to walk through the entire proposal process. This obviously isn't too big of a deal. The first method of walking through the governance process normally still works (and is probably safer since its closer to what will really happen), and I could also just use the etch cheatcode to overwrite the aave gov contracts code and replace it with a contract that just delegatecalls into my payload. |
Ah, I think I misunderstood what you were trying to do. Let me know if this is the correct explanation of the issue:
I don't know if that function sig / flow is exactly correct, but if that is representative of the problem then I agree that seems like a bug (contrary to my previous comment where I thought you were calling delegatecall directly from the test contract) |
Hmm this is not quite the flow I was using. It looks more like this:
The expectation I had was that calling |
Got it, makes sense. Agreed the current behavior is confusing, so I think there's two ways to improve it, let me know if you have other ideas:
IMO your use case is valid and potentially common so I'd support the second option |
+1, would also be useful to me, the general case seems to be:
|
There's now a delegate-prank tool available here: https://github.com/adhusson/delegate-prank edit: updated interface // c will delegatecall dest.fn(args)
delegatePrank(c,address(dest),abi.encodeCall(fn,(args))); |
Actually the idea of pranking delegatecall() can be super helpful, especially when you easily (atleast easier) want to overwrite storage. From what I know, this had to be done by getting the storage slot. It got even more complicated when you wanted to overwrite structs etc. I made an example which overwrites the owner that is stored as a value mapped to a random number in a mapping of numbers => address (to show how it can make possible complex things easier). If there was an easier way to manipulate storage other than by getting the slots please let me know haha.
Your code pretty much does what's needed, however, it should also change back the contract code to the original. Here's an example on how I did that and how a // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
/**
This contract is deployed by the user.
It has to have the exact same storage structure than the original.
In this simple example we want to change the variable `owner[0]` in `AAveGovernor`.
Previously, as far as I know, we'd have to get the storage slot and overwrite it.
By delegatecalling it temporarily as a proxy to our copy, we can easily do this without the previous step.
*/
contract AaveGovernorCopy {
mapping(uint => address) public owners;
// This function does not exist on AaveGovernor
function changeOwner(address _newOwner) external {
owners[19193984145157575157191571358] = _newOwner;
}
}
/**
This contract is the "main" contract where we want to overwrite the ownership.
Our goal is to easily (or easier) change the owner even though `AaveGovernor` doesn't have a function to do that.
To make things more complicated, the owner isn't stored in a simple address state variable, but a nice mapping.
And to make it more complicated, it's assigned to an annoying number.
*/
contract AaveGovernor {
mapping(uint => address) public owners;
constructor() {
owners[19193984145157575157191571358] = address(1);
}
}
/**
This is the temporary proxy contract that will replace the main contract (`AAveGovernor`) temporarily.
*/
contract PrankDelegator {
function prankDelegate(address _to, bytes memory _data) external returns(bool _success, bytes memory _returnData) {
return _to.delegatecall(_data);
}
}
// Our test contract
contract ContractTest is Test {
AaveGovernor aaveGovernor = new AaveGovernor();
AaveGovernorCopy aaveGovernorCopy = new AaveGovernorCopy();
PrankDelegator prankDelegator;
// @dev Temporarily changes `_from` into a proxy which then delegatecalls to `_to` with `_data`.
// @param _from The address to delegate from
// @param _to The address to delegate to
// @param _data The data to delegate with to `_to`
function prankDelegate(address _from, address _to, bytes memory _data) internal returns(bool _success, bytes memory _returnData) {
bytes memory sourceCode;
sourceCode = _from.code; // Temporarily store the current code of `_from`.
vm.etch(_from, address(prankDelegator).code); // "Turn" `_from` into a temporary proxy `PrankDelegator` by replacing its code.
(_success, _returnData) = _from.call(abi.encodeWithSignature("prankDelegate(address,bytes)",_to,_data)); // Make `_from` delegate to `_to`.
vm.etch(_from, sourceCode); // Restore the code of `_from` to its previous code before turning it into a proxy.
}
function setUp() public {
// Create our three contracts.
// Theoretically you could store the bytecode of `PrankDelegator` and replace the to be delegated contract with that so
// that there's no need to deploy a new contract when doing tests or when a new cheatcode should be implemented.
aaveGovernor = new AaveGovernor();
aaveGovernorCopy = new AaveGovernorCopy();
prankDelegator = new PrankDelegator();
// No need to label the delegator as we won't call it but only delegatecall to it but from the main contract.
vm.label(address(aaveGovernor), "Aave");
vm.label(address(aaveGovernorCopy), "Fake Aave");
}
// Test changing the owner in `AaveGovernor` without using a prank but instead turning it into a temporary proxy.
function testChangeOwner() public {
// Check and log the current owner.
require(aaveGovernor.owners(19193984145157575157191571358) == address(1), "Owner is not 1");
console.logAddress(aaveGovernor.owners(19193984145157575157191571358));
// Now do the `prankDelegate()` call.
prankDelegate(address(aaveGovernor), address(aaveGovernorCopy), abi.encodeWithSignature("changeOwner(address)",0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF));
// Check and log the new owner.
require(aaveGovernor.owners(19193984145157575157191571358) == 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF, "Owner is not 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF");
console.logAddress(aaveGovernor.owners(19193984145157575157191571358));
}
}
More complex result with proof that |
@ckksec Nice, I went a step further and now the bytecode is swapped back right before the contract does the delegatecall. |
This feature is 100% needed. The delegate caller being the test is unexpected and useless. |
cheatcodes
): add vm.delegatePrank
to execute a delegatecall
in the context of the pranked address
Hey @zerosnacks if this is available, I'd like to take it, please. Could you please answer these two questions for me and provide any other starters or tips?
Thank you. |
Hi @EdwardJES, I think the proposed API would be to update This is the related foundry/crates/cheatcodes/src/evm/prank.rs Lines 47 to 73 in 96105b4
It would involve modifying the prank API. From @mds1
It may require some experimentation so having an early draft could be beneficial for feedback. |
Hey @zerosnacks, I've compiled an early draft here at #8863. I thought I'd at least validate this logic before spinning up a PR. (@mds1 you may also be interested ) Ty 🙏 |
Component
Forge
Have you ensured that all of these are up to date?
What version of Foundry are you on?
forge 0.1.0 (d08a59e 2022-03-01T00:28:36.621137610+00:00)
What command(s) is the bug in?
forge test
Operating System
Linux
Describe the bug
When attempting to perform a delegate call from a test while a prank is enabled, it does not execute in the context of the pranked address. This likely is not the expected behavior for users.
The text was updated successfully, but these errors were encountered: