@0x/contracts-zero-ex: Address review feedback.
`@0x/contracts-zero-ex`: Add target implementation address to `rollback()`. `@0x/contracts-zero-ex`: Add storage ID uniqueness test. `@0x/contracts-zero-ex`: Add rollback history querying functions to `SimpleFunctionRegistry`.
This commit is contained in:
@@ -23,16 +23,15 @@ library LibSimpleFunctionRegistryRichErrors {
|
||||
|
||||
// solhint-disable func-name-mixedcase
|
||||
|
||||
function NoRollbackHistoryError(
|
||||
bytes4 selector
|
||||
)
|
||||
function NotInRollbackHistoryError(bytes4 selector, address targetImpl)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodeWithSelector(
|
||||
bytes4(keccak256("NoRollbackHistoryError(bytes4)")),
|
||||
selector
|
||||
bytes4(keccak256("NotInRollbackHistoryError(bytes4,address)")),
|
||||
selector,
|
||||
targetImpl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,16 @@ contract SimpleFunctionRegistry is
|
||||
_extend(this.extendSelf.selector, impl);
|
||||
// Register the rollback function.
|
||||
_extend(this.rollback.selector, impl);
|
||||
// Register getters.
|
||||
_extend(this.getRollbackLength.selector, impl);
|
||||
_extend(this.getRollbackEntryAtIndex.selector, impl);
|
||||
}
|
||||
|
||||
/// @dev Roll back to the last implementation of a function.
|
||||
/// @dev Roll back to a prior implementation of a function.
|
||||
/// Only directly callable by an authority.
|
||||
/// @param selector The function selector.
|
||||
function rollback(bytes4 selector)
|
||||
/// @param targetImpl The address of an older implementation of the function.
|
||||
function rollback(bytes4 selector, address targetImpl)
|
||||
external
|
||||
override
|
||||
onlyOwner
|
||||
@@ -65,17 +69,31 @@ contract SimpleFunctionRegistry is
|
||||
LibProxyStorage.Storage storage proxyStor
|
||||
) = _getStorages();
|
||||
|
||||
address currentImpl = proxyStor.impls[selector];
|
||||
if (currentImpl == targetImpl) {
|
||||
// Do nothing if already at targetImpl.
|
||||
return;
|
||||
}
|
||||
// Walk history backwards until we find the target implementation.
|
||||
address[] storage history = stor.implHistory[selector];
|
||||
if (history.length == 0) {
|
||||
uint256 i = history.length;
|
||||
for (; i > 0; --i) {
|
||||
address impl = history[i - 1];
|
||||
history.pop();
|
||||
if (impl == targetImpl) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == 0) {
|
||||
_rrevert(
|
||||
LibSimpleFunctionRegistryRichErrors.NoRollbackHistoryError(selector)
|
||||
LibSimpleFunctionRegistryRichErrors.NotInRollbackHistoryError(
|
||||
selector,
|
||||
targetImpl
|
||||
)
|
||||
);
|
||||
}
|
||||
address impl = history[history.length - 1];
|
||||
address oldImpl = proxyStor.impls[selector];
|
||||
proxyStor.impls[selector] = impl;
|
||||
history.pop();
|
||||
emit ProxyFunctionUpdated(selector, oldImpl, impl);
|
||||
proxyStor.impls[selector] = targetImpl;
|
||||
emit ProxyFunctionUpdated(selector, currentImpl, targetImpl);
|
||||
}
|
||||
|
||||
/// @dev Register or replace a function.
|
||||
@@ -102,6 +120,33 @@ contract SimpleFunctionRegistry is
|
||||
_extend(selector, impl);
|
||||
}
|
||||
|
||||
/// @dev Retrieve the length of the rollback history for a function.
|
||||
/// @param selector The function selector.
|
||||
/// @return rollbackLength The number of items in the rollback history for
|
||||
/// the function.
|
||||
function getRollbackLength(bytes4 selector)
|
||||
external
|
||||
override
|
||||
view
|
||||
returns (uint256 rollbackLength)
|
||||
{
|
||||
return LibSimpleFunctionRegistryStorage.getStorage().implHistory[selector].length;
|
||||
}
|
||||
|
||||
/// @dev Retrieve an entry in the rollback history for a function.
|
||||
/// @param selector The function selector.
|
||||
/// @param idx The index in the rollback history.
|
||||
/// @return impl An implementation address for the function at
|
||||
/// index `idx`.
|
||||
function getRollbackEntryAtIndex(bytes4 selector, uint256 idx)
|
||||
external
|
||||
override
|
||||
view
|
||||
returns (address impl)
|
||||
{
|
||||
return LibSimpleFunctionRegistryStorage.getStorage().implHistory[selector][idx];
|
||||
}
|
||||
|
||||
/// @dev Register or replace a function.
|
||||
/// @param selector The function selector.
|
||||
/// @param impl The implementation contract for the function.
|
||||
|
||||
@@ -20,7 +20,6 @@ pragma solidity ^0.6.5;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
|
||||
import "../storage/LibProxyStorage.sol";
|
||||
import "../errors/LibCommonRichErrors.sol";
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
pragma solidity ^0.6.5;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../interfaces/IZeroExBootstrapper.sol";
|
||||
|
||||
|
||||
/// @dev Basic interface for a feature contract.
|
||||
interface IFeature {
|
||||
|
||||
@@ -31,9 +31,10 @@ interface ISimpleFunctionRegistry {
|
||||
/// @param newImpl The replacement implementation contract address.
|
||||
event ProxyFunctionUpdated(bytes4 indexed selector, address oldImpl, address newImpl);
|
||||
|
||||
/// @dev Roll back to the last implementation of a function.
|
||||
/// @dev Roll back to a prior implementation of a function.
|
||||
/// @param selector The function selector.
|
||||
function rollback(bytes4 selector) external;
|
||||
/// @param targetImpl The address of an older implementation of the function.
|
||||
function rollback(bytes4 selector, address targetImpl) external;
|
||||
|
||||
/// @dev Register or replace a function.
|
||||
/// @param selector The function selector.
|
||||
@@ -45,4 +46,23 @@ interface ISimpleFunctionRegistry {
|
||||
/// @param selector The function selector.
|
||||
/// @param impl The implementation contract for the function.
|
||||
function extendSelf(bytes4 selector, address impl) external;
|
||||
|
||||
/// @dev Retrieve the length of the rollback history for a function.
|
||||
/// @param selector The function selector.
|
||||
/// @return rollbackLength The number of items in the rollback history for
|
||||
/// the function.
|
||||
function getRollbackLength(bytes4 selector)
|
||||
external
|
||||
view
|
||||
returns (uint256 rollbackLength);
|
||||
|
||||
/// @dev Retrieve an entry in the rollback history for a function.
|
||||
/// @param selector The function selector.
|
||||
/// @param idx The index in the rollback history.
|
||||
/// @return impl An implementation address for the function at
|
||||
/// index `idx`.
|
||||
function getRollbackEntryAtIndex(bytes4 selector, uint256 idx)
|
||||
external
|
||||
view
|
||||
returns (address impl);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ contract BasicMigration {
|
||||
|
||||
// Disable the `extendSelf()` function by rolling it back to zero.
|
||||
ISimpleFunctionRegistry(address(zeroEx))
|
||||
.rollback(ISimpleFunctionRegistry.extendSelf.selector);
|
||||
.rollback(ISimpleFunctionRegistry.extendSelf.selector, address(0));
|
||||
|
||||
// Call the _postInitialize hook.
|
||||
_postInitialize(zeroEx);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils';
|
||||
import { hexUtils, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils';
|
||||
import { BigNumber, hexUtils, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { basicMigrateAsync } from '../utils/migration';
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
ITestSimpleFunctionRegistryFeatureContract,
|
||||
TestSimpleFunctionRegistryFeatureImpl1Contract,
|
||||
TestSimpleFunctionRegistryFeatureImpl2Contract,
|
||||
ZeroExContract,
|
||||
} from '../wrappers';
|
||||
|
||||
blockchainTests.resets('SimpleFunctionRegistry feature', env => {
|
||||
const { NULL_ADDRESS } = constants;
|
||||
const notOwner = randomAddress();
|
||||
let owner: string;
|
||||
let zeroEx: ZeroExContract;
|
||||
let registry: ISimpleFunctionRegistryContract;
|
||||
let testFnSelector: string;
|
||||
let testFeature: ITestSimpleFunctionRegistryFeatureContract;
|
||||
@@ -23,7 +25,7 @@ blockchainTests.resets('SimpleFunctionRegistry feature', env => {
|
||||
|
||||
before(async () => {
|
||||
[owner] = await env.getAccountAddressesAsync();
|
||||
const zeroEx = await basicMigrateAsync(owner, env.provider, env.txDefaults);
|
||||
zeroEx = await basicMigrateAsync(owner, env.provider, env.txDefaults);
|
||||
registry = new ISimpleFunctionRegistryContract(zeroEx.address, env.provider, {
|
||||
...env.txDefaults,
|
||||
from: owner,
|
||||
@@ -50,17 +52,24 @@ blockchainTests.resets('SimpleFunctionRegistry feature', env => {
|
||||
});
|
||||
|
||||
it('`rollback()` cannot be called by a non-owner', async () => {
|
||||
const tx = registry.rollback(hexUtils.random(4)).callAsync({ from: notOwner });
|
||||
const tx = registry.rollback(hexUtils.random(4), NULL_ADDRESS).callAsync({ from: notOwner });
|
||||
return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner));
|
||||
});
|
||||
|
||||
it('`rollback()` reverts for unregistered function', async () => {
|
||||
const tx = registry.rollback(testFnSelector).awaitTransactionSuccessAsync();
|
||||
it('`rollback()` to non-zero impl reverts for unregistered function', async () => {
|
||||
const rollbackAddress = randomAddress();
|
||||
const tx = registry.rollback(testFnSelector, rollbackAddress).awaitTransactionSuccessAsync();
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.SimpleFunctionRegistry.NoRollbackHistoryError(testFnSelector),
|
||||
new ZeroExRevertErrors.SimpleFunctionRegistry.NotInRollbackHistoryError(testFnSelector, rollbackAddress),
|
||||
);
|
||||
});
|
||||
|
||||
it('`rollback()` to zero impl succeeds for unregistered function', async () => {
|
||||
await registry.rollback(testFnSelector, NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
const impl = await zeroEx.getFunctionImplementation(testFnSelector).callAsync();
|
||||
expect(impl).to.eq(NULL_ADDRESS);
|
||||
});
|
||||
|
||||
it('owner can add a new function with `extend()`', async () => {
|
||||
const { logs } = await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
verifyEventsFromLogs(
|
||||
@@ -79,7 +88,7 @@ blockchainTests.resets('SimpleFunctionRegistry feature', env => {
|
||||
expect(r).to.bignumber.eq(1338);
|
||||
});
|
||||
|
||||
it('owner can unset a function with `extend()`', async () => {
|
||||
it('owner can zero a function with `extend()`', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, constants.NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
return expect(testFeature.testFn().callAsync()).to.revertWith(
|
||||
@@ -87,32 +96,77 @@ blockchainTests.resets('SimpleFunctionRegistry feature', env => {
|
||||
);
|
||||
});
|
||||
|
||||
it('owner can rollback a new function to unset', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
const { logs } = await registry.rollback(testFnSelector).awaitTransactionSuccessAsync();
|
||||
verifyEventsFromLogs(
|
||||
logs,
|
||||
[{ selector: testFnSelector, oldImpl: testFeatureImpl1.address, newImpl: NULL_ADDRESS }],
|
||||
ISimpleFunctionRegistryEvents.ProxyFunctionUpdated,
|
||||
);
|
||||
return expect(testFeature.testFn().callAsync()).to.revertWith(
|
||||
new ZeroExRevertErrors.Proxy.NotImplementedError(testFnSelector),
|
||||
);
|
||||
});
|
||||
|
||||
it('owner can rollback a function to a previous version', async () => {
|
||||
it('can query rollback history', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, testFeatureImpl2.address).awaitTransactionSuccessAsync();
|
||||
await registry.rollback(testFnSelector).awaitTransactionSuccessAsync();
|
||||
const r = await testFeature.testFn().callAsync();
|
||||
expect(r).to.bignumber.eq(1337);
|
||||
await registry.extend(testFnSelector, NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
const rollbackLength = await registry.getRollbackLength(testFnSelector).callAsync();
|
||||
expect(rollbackLength).to.bignumber.eq(3);
|
||||
const entries = await Promise.all(
|
||||
[...new Array(rollbackLength.toNumber())].map((v, i) =>
|
||||
registry.getRollbackEntryAtIndex(testFnSelector, new BigNumber(i)).callAsync(),
|
||||
),
|
||||
);
|
||||
expect(entries).to.deep.eq([NULL_ADDRESS, testFeatureImpl1.address, testFeatureImpl2.address]);
|
||||
});
|
||||
|
||||
it('owner can rollback an unset function to a previous version', async () => {
|
||||
it('owner can rollback a function to zero', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, constants.NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
await registry.rollback(testFnSelector).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, testFeatureImpl2.address).awaitTransactionSuccessAsync();
|
||||
const { logs } = await registry.rollback(testFnSelector, NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
verifyEventsFromLogs(
|
||||
logs,
|
||||
[{ selector: testFnSelector, oldImpl: testFeatureImpl2.address, newImpl: NULL_ADDRESS }],
|
||||
ISimpleFunctionRegistryEvents.ProxyFunctionUpdated,
|
||||
);
|
||||
const rollbackLength = await registry.getRollbackLength(testFnSelector).callAsync();
|
||||
expect(rollbackLength).to.bignumber.eq(0);
|
||||
return expect(testFeature.testFn().callAsync()).to.revertWith(
|
||||
new ZeroExRevertErrors.Proxy.NotImplementedError(testFnSelector),
|
||||
);
|
||||
});
|
||||
|
||||
it('owner can rollback a function to the prior version', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, testFeatureImpl2.address).awaitTransactionSuccessAsync();
|
||||
await registry.rollback(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
const r = await testFeature.testFn().callAsync();
|
||||
expect(r).to.bignumber.eq(1337);
|
||||
const rollbackLength = await registry.getRollbackLength(testFnSelector).callAsync();
|
||||
expect(rollbackLength).to.bignumber.eq(1);
|
||||
});
|
||||
|
||||
it('owner can rollback a zero function to the prior version', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl2.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, constants.NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
await registry.rollback(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
const r = await testFeature.testFn().callAsync();
|
||||
expect(r).to.bignumber.eq(1337);
|
||||
const rollbackLength = await registry.getRollbackLength(testFnSelector).callAsync();
|
||||
expect(rollbackLength).to.bignumber.eq(2);
|
||||
});
|
||||
|
||||
it('owner can rollback a function to a much older version', async () => {
|
||||
await registry.extend(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, testFeatureImpl2.address).awaitTransactionSuccessAsync();
|
||||
await registry.rollback(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
const r = await testFeature.testFn().callAsync();
|
||||
expect(r).to.bignumber.eq(1337);
|
||||
const rollbackLength = await registry.getRollbackLength(testFnSelector).callAsync();
|
||||
expect(rollbackLength).to.bignumber.eq(1);
|
||||
});
|
||||
|
||||
it('owner cannot rollback a function to a version not in history', async () => {
|
||||
await registry.extend(testFnSelector, NULL_ADDRESS).awaitTransactionSuccessAsync();
|
||||
await registry.extend(testFnSelector, testFeatureImpl2.address).awaitTransactionSuccessAsync();
|
||||
const tx = registry.rollback(testFnSelector, testFeatureImpl1.address).awaitTransactionSuccessAsync();
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.SimpleFunctionRegistry.NotInRollbackHistoryError(
|
||||
testFnSelector,
|
||||
testFeatureImpl1.address,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
33
contracts/zero-ex/test/storage_uniqueness_test.ts
Normal file
33
contracts/zero-ex/test/storage_uniqueness_test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe } from '@0x/contracts-test-utils';
|
||||
import { readdir, readFile } from 'fs';
|
||||
import { basename, resolve } from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
describe('Storage ID uniqueness test', () => {
|
||||
const STORAGE_SOURCES_DIR = resolve(__dirname, '../../contracts/src/storage');
|
||||
async function findStorageIdFromSourceFileAsync(path: string): Promise<string | void> {
|
||||
const contents = await promisify(readFile)(path, { encoding: 'utf-8' });
|
||||
const m = /STORAGE_ID\s*=\s*(0x[a-fA-F0-9]{64})/m.exec(contents);
|
||||
if (m) {
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
|
||||
it('all STORAGE_IDs are unique in storage libraries', async () => {
|
||||
const sourcePaths = (await promisify(readdir)(STORAGE_SOURCES_DIR))
|
||||
.filter(p => p.endsWith('.sol'))
|
||||
.map(p => resolve(STORAGE_SOURCES_DIR, p));
|
||||
const storageIds = await Promise.all(sourcePaths.map(async p => findStorageIdFromSourceFileAsync(p)));
|
||||
for (let i = 0; i < storageIds.length; ++i) {
|
||||
const storageId = storageIds[i];
|
||||
for (let j = 0; j < storageIds.length; ++j) {
|
||||
if (i !== j && storageId === storageIds[j]) {
|
||||
throw new Error(
|
||||
`Found duplicate STORAGE_ID ${storageId} ` +
|
||||
`in files ${basename(sourcePaths[i])}, ${basename(sourcePaths[j])}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -50,13 +50,13 @@ blockchainTests.resets('ZeroEx contract', env => {
|
||||
});
|
||||
|
||||
it('can attach ether to a call', async () => {
|
||||
const wei = Math.floor(Math.random() * 100);
|
||||
const wei = Math.floor(Math.random() * 100 + 1);
|
||||
const receipt = await testFeature.payableFn().awaitTransactionSuccessAsync({ value: wei });
|
||||
verifyEventsFromLogs(receipt.logs, [{ value: new BigNumber(wei) }], TestZeroExFeatureEvents.PayableFnCalled);
|
||||
});
|
||||
|
||||
it('reverts when attaching ether to a non-payable function', async () => {
|
||||
const wei = Math.floor(Math.random() * 100);
|
||||
const wei = Math.floor(Math.random() * 100 + 1);
|
||||
const tx = testFeature.notPayableFn().awaitTransactionSuccessAsync({ value: wei });
|
||||
// This will cause an empty revert.
|
||||
return expect(tx).to.be.rejectedWith('revert');
|
||||
|
||||
Reference in New Issue
Block a user