Skip to content
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

[StorageReading] Invoke external viewer contract via static delegatecall #31

Merged
merged 11 commits into from
Jun 12, 2020
62 changes: 61 additions & 1 deletion contracts/StorageAccessible.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
pragma solidity ^0.5.2;


/// @title StorageAccessible - generic base contract that allows callers to access all internal storage.
contract StorageAccessible {
bytes4 public constant SIMULATE_DELEGATECALL_INTERNAL_SELECTOR = bytes4(
keccak256("simulateDelegatecallInternal(address,bytes)")
);

/**
* @dev Reads `length` bytes of storage in the currents contract
* @param offset - the offset in the current contract's storage in words to start reading from
Expand All @@ -23,4 +26,61 @@ contract StorageAccessible {
}
return result;
}

/**
* @dev Performs a delegetecall on a targetContract in the context of self.
* Internally reverts execution to avoid side effects (making it static). Catches revert and returns encoded result as bytes.
* @param targetContract Address of the contract containing the code to execute.
* @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
*/
function simulateDelegatecall(
address targetContract,
bytes memory calldataPayload
) public returns (bytes memory) {
bytes memory innerCall = abi.encodeWithSelector(
SIMULATE_DELEGATECALL_INTERNAL_SELECTOR,
targetContract,
calldataPayload
);
(, bytes memory response) = address(this).call(innerCall);
// We try to detect if the delegatecall itself reverted (with a reason string).
// In this case we use this reason to revert the top level call.
if (isRevert(response)) {
revertWith(response);
} else {
return response;
}
}

/**
* @dev Performs a delegetecall on a targetContract in the context of self.
* Internally reverts execution to avoid side effects (making it static). Returns encoded result as revert message.
* @param targetContract Address of the contract containing the code to execute.
* @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
*/
function simulateDelegatecallInternal(
address targetContract,
bytes memory calldataPayload
) public returns (bytes memory) {
(, bytes memory response) = targetContract.delegatecall(
calldataPayload
);
revertWith(response);
}

function isRevert(bytes memory result) public pure returns (bool) {
// Check if result starts with the method selector "Error(string)"
return
result.length > 4 &&
result[0] == 0x08 &&
result[1] == 0xc3 &&
result[2] == 0x79 &&
result[3] == 0xa0;
Copy link
Contributor

@nlordell nlordell Jun 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One tiny suggestion (but I'm not sure its worth doing) is maybe this check should be stricter, specifically:

  • First four bytes must be 0x08c379a0
  • Next 32-byte word must be 0x20 (string offset).
  • Read next 32-byte word as len
  • The result.length = 4 + k*32 such that k == ceil(len / 32) + 2

But that might be overkill.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your approach is 100x better.

}

function revertWith(bytes memory response) public pure {
assembly {
revert(add(response, 0x20), mload(response))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try to open a issue with the solidity team to get a way to do this without assembly (the revert of bytes)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever! Do I understand this to mean that the response data is the revert message?

Copy link

@rmeissner rmeissner Jun 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a revert message works different. Solidity encoded the revert message like a call to a function Error(string) (see https://solidity.readthedocs.io/en/v0.5.3/control-structures.html#error-handling-assert-require-revert-and-exceptions). Currently it is not possible to return raw bytes (without any additional encoding). Therefore it is required to use assembly for this.

@fleupold if required I could open an issue on the solidity repo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the issue would request support for calling revert with bytes as arguments inside solidity right? I'm not sure this is the intended use of revert (we are kind of shoehorning it a bit here) and thus I'm fine with using assembly for this.

Do you have another use case where we would legitimately want to return bytes from a revert?

Feel free to open issue regardless, we can see what the solidity community thinks about.

}
}
}
32 changes: 31 additions & 1 deletion contracts/test/StorageAccessibleWrapper.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
pragma solidity ^0.5.2;
import "../StorageAccessible.sol";


contract StorageAccessibleWrapper is StorageAccessible {
struct FooBar {
uint256 foo;
Expand Down Expand Up @@ -48,3 +47,34 @@ contract StorageAccessibleWrapper is StorageAccessible {
foobar = FooBar({foo: foo_, bar: bar_});
}
}

/**
* Defines reader methods on StorageAccessibleWrapper that can be later executed
* in the context of a previously deployed instance
*/
contract ExternalStorageReader {
// Needs same storage layout as the contract it is reading from
uint256 foo;

function getFoo() public view returns (uint256) {
return foo;
}

function setAndGetFoo(uint256 foo_) public returns (uint256) {
foo = foo_;
return foo;
}

function doRevert() public pure {
revert("Foo");
}

function invokeDoRevertViaStorageAccessible(StorageAccessible target)
public
{
target.simulateDelegatecall(
address(this),
abi.encodeWithSignature("doRevert()")
);
}
}
115 changes: 82 additions & 33 deletions test/storage_accessible.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,100 @@
const StorageAccessibleWrapper = artifacts.require('StorageAccessibleWrapper')
const ExternalStorageReader = artifacts.require('ExternalStorageReader')

const truffleAssert = require('truffle-assertions')

contract('StorageAccessible', () => {
const fromHex = string => parseInt(string, 16)
const keccak = numbers => web3.utils.soliditySha3(
...numbers.map(v => ({ type: 'uint256', value: v }))
)

it('can read statically sized words', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setFoo(42)
describe('getStorageAt', async () => {
it('can read statically sized words', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setFoo(42)

assert.equal(42, await instance.getStorageAt(await instance.SLOT_FOO(), 1))
})
it('can read fields that are packed into single storage slot', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setBar(7)
await instance.setBam(13)
assert.equal(42, await instance.getStorageAt(await instance.SLOT_FOO(), 1))
})
it('can read fields that are packed into single storage slot', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setBar(7)
await instance.setBam(13)

const data = await instance.getStorageAt(await instance.SLOT_BAR(), 1)
const data = await instance.getStorageAt(await instance.SLOT_BAR(), 1)

assert.equal(7, fromHex(data.slice(34, 66)))
assert.equal(13, fromHex(data.slice(18, 34)))
})
it('can read arrays in one go', async () => {
const instance = await StorageAccessibleWrapper.new()
const slot = await instance.SLOT_BAZ()
await instance.setBaz([1, 2])
assert.equal(7, fromHex(data.slice(34, 66)))
assert.equal(13, fromHex(data.slice(18, 34)))
})
it('can read arrays in one go', async () => {
const instance = await StorageAccessibleWrapper.new()
const slot = await instance.SLOT_BAZ()
await instance.setBaz([1, 2])

const length = await instance.getStorageAt(slot, 1)
assert.equal(fromHex(length), 2)
const length = await instance.getStorageAt(slot, 1)
assert.equal(fromHex(length), 2)

const data = await instance.getStorageAt(keccak([slot]), length)
assert.equal(1, fromHex(data.slice(2, 66)))
assert.equal(2, fromHex(data.slice(66, 130)))
})
it('can read mappings', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setQuxKeyValue(42, 69)
assert.equal(69, await instance.getStorageAt(keccak([42, await instance.SLOT_QUX()]), 1))
const data = await instance.getStorageAt(keccak([slot]), length)
assert.equal(1, fromHex(data.slice(2, 66)))
assert.equal(2, fromHex(data.slice(66, 130)))
})
it('can read mappings', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setQuxKeyValue(42, 69)
assert.equal(69, await instance.getStorageAt(keccak([42, await instance.SLOT_QUX()]), 1))
})

it('can read structs', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setFoobar(19, 21)

const packed = await instance.getStorageAt(await instance.SLOT_FOOBAR(), 10)
assert.equal(19, fromHex(packed.slice(2, 66)))
assert.equal(21, fromHex(packed.slice(66, 130)))
})
})

it('can read structs', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setFoobar(19, 21)
describe('simulateDelegatecall', async () => {
it('can invoke a function in the context of a previously deployed contract', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setFoo(42)

// Deploy and use reader contract to access foo
const reader = await ExternalStorageReader.new()
const getFooCall = reader.contract.methods.getFoo().encodeABI()
const result = await instance.simulateDelegatecall.call(reader.address, getFooCall)
assert.equal(42, fromHex(result))
})

it('can simulate a function with side effects (without executing)', async () => {
const instance = await StorageAccessibleWrapper.new()
await instance.setFoo(42)

// Deploy and use reader contract to simulate setting foo
const reader = await ExternalStorageReader.new()
const replaceFooCall = reader.contract.methods.setAndGetFoo(69).encodeABI()
let result = await instance.simulateDelegatecall.call(reader.address, replaceFooCall)
assert.equal(69, fromHex(result))

// Make sure foo is not actually changed
const getFooCall = reader.contract.methods.getFoo().encodeABI()
result = await instance.simulateDelegatecall.call(reader.address, getFooCall)
assert.equal(42, fromHex(result))
})

it('can simulate a function that reverts', async () => {
const instance = await StorageAccessibleWrapper.new()

const reader = await ExternalStorageReader.new()
const doRevertCall = reader.contract.methods.doRevert().encodeABI()
truffleAssert.reverts(instance.simulateDelegatecall.call(reader.address, doRevertCall))
})

it('allows detection of reverts when invoked from other smart contract', async () => {
const instance = await StorageAccessibleWrapper.new()

const packed = await instance.getStorageAt(await instance.SLOT_FOOBAR(), 10)
assert.equal(19, fromHex(packed.slice(2, 66)))
assert.equal(21, fromHex(packed.slice(66, 130)))
const reader = await ExternalStorageReader.new()
truffleAssert.reverts(reader.invokeDoRevertViaStorageAccessible(instance.address))
})
})
})