Skip to content
This repository has been archived by the owner. 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
Copy link
Contributor Author

@frangio 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
Member

@spalladino spalladino commented Apr 12, 2018

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());

This comment has been minimized.

@spalladino

spalladino Apr 12, 2018
Member

Wouldn't this break the createAndCall methods?

This comment has been minimized.

@facuspagnuolo

facuspagnuolo Apr 12, 2018
Contributor

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

This comment has been minimized.

@spalladino

spalladino Apr 13, 2018
Member

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

This comment has been minimized.

@frangio

frangio Apr 13, 2018
Author Contributor

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 this to Backlog in MVP via automation 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
@frangio frangio removed this from Backlog in MVP Apr 24, 2018
@@ -28,8 +28,11 @@ contract OwnedUpgradeabilityProxy is UpgradeabilityProxy {
* @dev Throws if called by any account other than the owner.

This comment has been minimized.

@fiiiu

fiiiu May 7, 2018
Contributor

update docs to reflect current caller-dependent behavior.

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

/**
* @dev Sets the address of the owner
*/

This comment has been minimized.

@fiiiu

fiiiu May 7, 2018
Contributor

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

@frangio frangio force-pushed the feature/transparent-proxy branch from 8f81183 to 9f590ce May 8, 2018
@frangio
Copy link
Contributor Author

@frangio 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
@frangio frangio requested a review from facuspagnuolo May 10, 2018
@frangio frangio force-pushed the feature/transparent-proxy branch from e007c10 to 3c1f279 May 10, 2018
@frangio
Copy link
Contributor Author

@frangio frangio commented May 10, 2018

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

@frangio
Copy link
Contributor Author

@frangio 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

@facuspagnuolo facuspagnuolo commented May 10, 2018

@frangio would you mind rebasing from master?

Copy link
Contributor

@facuspagnuolo facuspagnuolo left a comment

Thanks fran! Looking really good, left some minor comments

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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

🙏

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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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

This comment has been minimized.

@frangio

frangio May 11, 2018
Author Contributor

#98

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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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

This comment has been minimized.

@frangio

frangio May 11, 2018
Author Contributor

Yeah! See this comment: #36 (comment)

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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

This comment has been minimized.

@alcuadrado

alcuadrado May 13, 2018

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 {

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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

This comment has been minimized.

@fiiiu

fiiiu May 11, 2018
Contributor

+1

*/
function () payable external {
_fallback();
}

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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 {

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

let's use setProxyOwner here

This comment has been minimized.

@fiiiu

fiiiu May 11, 2018
Contributor

+1

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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

👍

This comment has been minimized.


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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

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

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

This comment has been minimized.

@facuspagnuolo

facuspagnuolo May 11, 2018
Contributor

👏

*/
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

This comment has been minimized.

@fiiiu

fiiiu May 11, 2018
Contributor

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 {

This comment has been minimized.

@fiiiu

fiiiu May 11, 2018
Contributor

+1

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

This comment has been minimized.

@fiiiu

fiiiu May 11, 2018
Contributor

+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 {

This comment has been minimized.

@fiiiu

fiiiu May 11, 2018
Contributor

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

This comment has been minimized.

@frangio

frangio May 11, 2018
Author Contributor

_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.

This comment has been minimized.

@frangio

frangio May 11, 2018
Author Contributor

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 frangio commented May 12, 2018

@fiiiu @facuspagnuolo All review comments addressed! 🚀

@maraoz
Copy link
Contributor

@maraoz 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
2 checks passed
2 checks passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
@frangio frangio deleted the feature/transparent-proxy branch May 13, 2018
@frangio frangio mentioned this pull request May 14, 2018
9 of 15 tasks complete
@facuspagnuolo facuspagnuolo removed this from the Backlog milestone Jun 4, 2018
spalladino added 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.
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

6 participants
You can’t perform that action at this time.