Skip to content
This repository has been archived by the owner on Jul 11, 2019. It is now read-only.

Transparent Proxy #36

Merged
merged 16 commits into from
May 13, 2018
Merged

Transparent Proxy #36

merged 16 commits into from
May 13, 2018

Conversation

frangio
Copy link
Contributor

@frangio frangio commented Apr 12, 2018

The idea explored here is to make the Proxy entirely transparent to a normal user, removing the conflicts between the public administrative interface of the proxy itself, and the interface of the underlying proxied contract.

For example, OwnedUpgradeabilityProxy cannot have a public function owner(), because the proxied contract may have a function with the same signature. If both the proxy and proxied contracts had a function owner(), any such call would be caught by the proxy and not be delegated.

We solve this situation essentially by modifying onlyProxyOwner to execute the administrative function if the caller is the proxyOwner, and otherwise delegate the call to the proxied contract.

https://github.com/zeppelinos/core/blob/8f81183f4de074fb2359ceab285c2cbf085a7c3a/contracts/upgradeability/OwnedUpgradeabilityProxy.sol#L30-L36

There is a caveat, which is that Proxy#implementation is now only accessible by the proxyOwner. When the proxy owner is a contract that manages upgrades, such as ProjectController, it should expose this information about its owned proxies.

https://github.com/zeppelinos/core/blob/8f81183f4de074fb2359ceab285c2cbf085a7c3a/contracts/ProjectController.sol#L163-L165

Similarly, OwnedUpgradeabilityProxy#proxyOwner cannot be queried by anyone, only by the proxyOwner. This makes it kind of redundant to expose proxyOwner via the owner contract (like we can do with implementation). If someone wished to query who is the owner for a particular proxy they would have to read the storage location directly, or look for proxy ownership events originating from its address.

I'd like to hear everyone's thoughts on this.

  • Possibly rename onlyProxyOwner as the semantics are different from the usual only* modifiers.
  • Remove getProxyOwner and test ownership changes differently.
  • Fix the failing OwnedUpgradeabilityProxy tests (see comment below).

@frangio frangio added the status:in progress Pull Request is a work in progress label Apr 12, 2018
@frangio
Copy link
Contributor Author

frangio commented Apr 12, 2018

The tests are failing because of the way OwnedUpgradeabilityProxy is being tested: the implementations behind the proxy are not actual contracts but account addresses.

But there is kind of a deeper reason why the previous tests may no longer be appropriate. For example one of the failing tests is "OwnedUpgradeabilityProxy upgradeTo: when the sender is not the owner reverts". This is no longer true in the "transparent" model. When the sender is not the owner the behavior will depend on the underlying implementation.

This fact makes me doubt if the transparent model is a good one.

@spalladino
Copy link
Contributor

There is another reason for this feature, and that is that function signature clashes are not that uncommon. A method identifier is 4 bytes, which is only 2^32 possible values. For reference, the 4byte directory has currently about 2^15 registered function names. The main problem is that, without the compiler detecting the clash, the underlying function will be uncallable without the developer noticing.

An alternative is to build this into a linter to check this kind of issues, but I'd still like to ensure no clashes at a contract level.

Another alternative (at contract level) would be to require an extra 256-bit parameter to be included in every call that should be directed to the proxy itself, thus artificially increasing the length of the signature; but I think I prefer your implementation.

* @dev Redefines Proxy's fallback to disallow delegation when caller is the proxy owner.
*/
function () public payable {
require(msg.sender != proxyOwner());
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this break the createAndCall methods?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it shouldn't since the factory performs the call and then transfers the ownership to the requested owner

Copy link
Contributor

Choose a reason for hiding this comment

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

Still, I believe it's a bit too brittle. I'd consider removing this check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wouldn't remove the check for this reason alone. I think the proxyOwner should only be allowed to use the proxy opaquely. If this breaks our current implementation of initialization, we may need to revise that part of the model.

I agree that it is brittle in this way and that it needs to be tackled, but I'm going to think of other solutions.

@facuspagnuolo facuspagnuolo added status:blocked Blocked issue and removed status:in progress Pull Request is a work in progress labels Apr 17, 2018
@facuspagnuolo facuspagnuolo added this to the v0.1.0 (Kernel MVP) milestone Apr 17, 2018
@frangio frangio modified the milestones: v0.1.0 (Kernel MVP), v0.2.0 Apr 24, 2018
@@ -28,8 +28,11 @@ contract OwnedUpgradeabilityProxy is UpgradeabilityProxy {
* @dev Throws if called by any account other than the owner.
Copy link
Contributor

Choose a reason for hiding this comment

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

update docs to reflect current caller-dependent behavior.

function implementation() public view returns (address) {
return _implementation();
}

/**
* @dev Sets the address of the owner
*/
Copy link
Contributor

@fiiiu fiiiu May 7, 2018

Choose a reason for hiding this comment

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

NIT: L59, setUpgradeabilityOwner -> _setUpgradeabilityOwner? See also calls in L24 & L73.

@frangio frangio force-pushed the feature/transparent-proxy branch from 8f81183 to 9f590ce Compare May 8, 2018 21:35
@frangio frangio added status:in progress Pull Request is a work in progress and removed status:blocked Blocked issue labels May 8, 2018
@frangio
Copy link
Contributor Author

frangio commented May 8, 2018

I've rebased the changes on master and I'm working on getting this running again. I still need to fix a few tests.

@frangio frangio requested a review from fiiiu May 10, 2018 18:59
@frangio frangio added status:review PR waiting for review and removed status:in progress Pull Request is a work in progress labels May 10, 2018
@frangio frangio force-pushed the feature/transparent-proxy branch from e007c10 to 3c1f279 Compare May 10, 2018 21:07
@frangio
Copy link
Contributor Author

frangio commented May 10, 2018

This is an important PR so please review it in detail. 🙏

@frangio
Copy link
Contributor Author

frangio commented May 10, 2018

The only thing I still have doubts about are the proxyOwner and getProxyOwner functions. The first one only works when the proxy owner calls it, and the second one returns the same AppManager address on which you must call the function, so it is all somewhat redundant. Someone other than the proxy owner can get this information by reading from storage:

const position = web3.sha3("org.zeppelinos.proxy.owner");
const proxyOwner = await web3.eth.getStorageAt(this.proxyAddress, position);

We should provide in the zos-lib package a helper to read from storage at this position. We could use this same helper in the tests, instead of the redundant getProxyOwner.

Thoughts?

@facuspagnuolo
Copy link
Contributor

@frangio would you mind rebasing from master?

Copy link
Contributor

@facuspagnuolo facuspagnuolo left a comment

Choose a reason for hiding this comment

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

Thanks fran! Looking really good, left some minor comments

@@ -3,3 +3,4 @@ build/
lib/
coverage/
coverage.json
.node-xmlhttprequest-sync-*
Copy link
Contributor

Choose a reason for hiding this comment

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

🙏

function delegatedFunction() external pure returns (bool) {
return true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

we should move all the mocks for testing purpose to a test folder and npm-ignore it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#98

*/
function getProxyOwner(OwnedUpgradeabilityProxy proxy) public view returns (address) {
return proxy.proxyOwner();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is kind of redundant, isn't it? We are asking the owner of the proxy to tell us the owner of the proxy :p
But I guess we will need it for consistency tho

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah! See this comment: #36 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with adding a helper to read the owner from storage directly :)

Choose a reason for hiding this comment

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

I'd go with both options. The helper is needed when BaseAppManager is not used, but it's somewhat obscure. If an app manager is used, the symmetry between getProxyOwner and getProxyImplementation would be helpful for newcomers.

pragma solidity ^0.4.21;


contract ClashingImplementation {
Copy link
Contributor

Choose a reason for hiding this comment

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

we can add an inline comment here explaining what is clashing in this contract

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

*/
function () payable external {
_fallback();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

can we move internal functions to the bottom of the contract?

/**
* @dev Sets the address of the owner
*/
function setUpgradeabilityOwner(address newProxyOwner) internal {
function _setUpgradeabilityOwner(address newProxyOwner) internal {
Copy link
Contributor

Choose a reason for hiding this comment

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

let's use setProxyOwner here

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

proxy.transferProxyOwnership(owner);
require(proxy.call.value(msg.value)(data));
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Choose a reason for hiding this comment

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

Nice catch @frangio


const proxyOwner = await this.proxy.proxyOwner()
it('assigns new proxy owner', async function () {
// [TODO] maybe we should test this by reading from storage
Copy link
Contributor

Choose a reason for hiding this comment

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

it would be too implementative in this context IMO... we are already checking storage works as expected

Copy link
Contributor

Choose a reason for hiding this comment

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

if you agree please remove the TODO note or add an issue to tackle it

assert.equal(value, '0x0000000000000000000000000000000011111142')
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

👏

*/
contract Proxy {
/**
* @dev Tells the address of the implementation where every call will be delegated.
* @return address of the implementation to which it will be delegated
* @return address of the implementation to which the fallback delegates all calls
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we also want a @dev line here? I understand that it will be redundant, but concerned about automated doc generation..

pragma solidity ^0.4.21;


contract ClashingImplementation {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

/**
* @dev Sets the address of the owner
*/
function setUpgradeabilityOwner(address newProxyOwner) internal {
function _setUpgradeabilityOwner(address newProxyOwner) internal {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@@ -52,7 +56,7 @@ contract UpgradeabilityProxy is Proxy {

/**
* @dev Upgrades the implementation address
* @param newImplementation The address of the new implementation to be set
* @param newImplementation representing the address of the new implementation to be set
*/
function _upgradeTo(address newImplementation) internal {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need both _upgradeTo() and _setImplementation() or is this just a naming issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_setImplementation() should be a dumb setter for the org.zeppelinos.proxy.implementation slot, which is why it's private. I'm going to move the other logic to _upgradeTo and add comments explaining.

Copy link
Contributor Author

@frangio frangio May 11, 2018

Choose a reason for hiding this comment

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

I ended up leaving the isContract check in the otherwise "dumb" setter because it's quite crucial and in fact it initially led me to a bug which was hard to pin down (but correctly caused the tests to fail).

@frangio
Copy link
Contributor Author

frangio commented May 12, 2018

@fiiiu @facuspagnuolo All review comments addressed! 🚀

@maraoz
Copy link
Contributor

maraoz commented May 13, 2018

Reviewed the code and all pending comments. LGTM.
I think the test helper for extracting the proxyOwner can be left out of scope of this PR, so I opened an issue for it, and we can address it in the future: #100
Merging, great work @frangio!

@maraoz maraoz merged commit 44851b1 into master May 13, 2018
@frangio frangio deleted the feature/transparent-proxy branch May 13, 2018 23:33
@frangio frangio mentioned this pull request May 14, 2018
15 tasks
@facuspagnuolo facuspagnuolo removed this from the Backlog milestone Jun 4, 2018
spalladino pushed a commit to OpenZeppelin/openzeppelin-sdk that referenced this pull request Aug 1, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
status:review PR waiting for review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants