-
Notifications
You must be signed in to change notification settings - Fork 18
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
Changes from 9 commits
9766a5a
1889e3a
c691898
863aafd
f5a5210
b44163b
8a8f2ae
be016ff
729d4b3
58f4ad2
71f7fcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
@@ -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; | ||
} | ||
|
||
function revertWith(bytes memory response) public pure { | ||
assembly { | ||
revert(add(response, 0x20), mload(response)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 @fleupold if required I could open an issue on the solidity repo There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the issue would request support for calling 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. |
||
} | ||
} | ||
} |
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)) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
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:
len
result.length = 4 + k*32
such thatk == ceil(len / 32) + 2
But that might be overkill.
There was a problem hiding this comment.
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.