diff --git a/contracts/StorageAccessible.sol b/contracts/StorageAccessible.sol index 6f7b63f..01c3ebd 100644 --- a/contracts/StorageAccessible.sol +++ b/contracts/StorageAccessible.sol @@ -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 @@ -23,4 +26,58 @@ 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); + bool innerSuccess = response[response.length - 1] == 0x01; + setLength(response, response.length - 1); + if (innerSuccess) { + return response; + } else { + revertWith(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 + * concatenated with the success flag of the inner call as a last byte. + * @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) { + (bool success, bytes memory response) = targetContract.delegatecall( + calldataPayload + ); + revertWith(abi.encodePacked(response, success)); + } + + function revertWith(bytes memory response) public pure { + assembly { + revert(add(response, 0x20), mload(response)) + } + } + + function setLength(bytes memory buffer, uint256 length) public pure { + assembly { + mstore(buffer, length) + } + } } diff --git a/contracts/test/StorageAccessibleWrapper.sol b/contracts/test/StorageAccessibleWrapper.sol index e4cc2b7..8ef68e8 100644 --- a/contracts/test/StorageAccessibleWrapper.sol +++ b/contracts/test/StorageAccessibleWrapper.sol @@ -1,7 +1,6 @@ pragma solidity ^0.5.2; import "../StorageAccessible.sol"; - contract StorageAccessibleWrapper is StorageAccessible { struct FooBar { uint256 foo; @@ -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(); + } + + function invokeDoRevertViaStorageAccessible(StorageAccessible target) + public + { + target.simulateDelegatecall( + address(this), + abi.encodeWithSignature("doRevert()") + ); + } +} diff --git a/test/storage_accessible.js b/test/storage_accessible.js index 3b0f0ee..4c30670 100644 --- a/test/storage_accessible.js +++ b/test/storage_accessible.js @@ -1,4 +1,7 @@ const StorageAccessibleWrapper = artifacts.require('StorageAccessibleWrapper') +const ExternalStorageReader = artifacts.require('ExternalStorageReader') + +const truffleAssert = require('truffle-assertions') contract('StorageAccessible', () => { const fromHex = string => parseInt(string, 16) @@ -6,46 +9,92 @@ contract('StorageAccessible', () => { ...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)) + }) }) })