Skip to content
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

Fix TransparentUpgradeableProxy's transparency #4154

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thirty-shrimps-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

`TransparentUpgradeableProxy`: fix transparency in case of selector clash with non-decodable calldata
Amxx marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 5 additions & 5 deletions contracts/proxy/transparent/ProxyAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract ProxyAdmin is Ownable {
*
* - This contract must be the admin of `proxy`.
*/
function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
function getProxyImplementation(ITransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("implementation()")) == 0x5c60da1b
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b");
Expand All @@ -33,7 +33,7 @@ contract ProxyAdmin is Ownable {
*
* - This contract must be the admin of `proxy`.
*/
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
function getProxyAdmin(ITransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
Expand All @@ -48,7 +48,7 @@ contract ProxyAdmin is Ownable {
*
* - This contract must be the current admin of `proxy`.
*/
function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
proxy.changeAdmin(newAdmin);
}

Expand All @@ -59,7 +59,7 @@ contract ProxyAdmin is Ownable {
*
* - This contract must be the admin of `proxy`.
*/
function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
function upgrade(ITransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}

Expand All @@ -72,7 +72,7 @@ contract ProxyAdmin is Ownable {
* - This contract must be the admin of `proxy`.
*/
function upgradeAndCall(
TransparentUpgradeableProxy proxy,
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
Expand Down
84 changes: 53 additions & 31 deletions contracts/proxy/transparent/TransparentUpgradeableProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ pragma solidity ^0.8.0;

import "../ERC1967/ERC1967Proxy.sol";

interface ITransparentUpgradeableProxy {
frangio marked this conversation as resolved.
Show resolved Hide resolved
event Upgraded(address indexed implementation);
event AdminChanged(address previousAdmin, address newAdmin);
function admin() external view returns (address);
function implementation() external view returns (address);
function changeAdmin(address) external;
function upgradeTo(address) external;
function upgradeToAndCall(address, bytes memory) payable external;
}

/**
* @dev This contract implements a proxy that is upgradeable by an admin.
*
Expand Down Expand Up @@ -36,75 +46,95 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
}

/**
* @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin.
* @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior
*/
modifier ifAdmin() {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
function _fallback() internal virtual override {
if (msg.sender == _getAdmin()) {
_;
bytes memory ret;
bytes4 selector = msg.sig;
if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) {
ret = _dispatchUpgradeTo();
} else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
ret = _dispatchUpgradeToAndCall();
} else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) {
ret = _dispatchChangeAdmin();
} else if (selector == ITransparentUpgradeableProxy.admin.selector) {
ret = _dispatchAdmin();
} else if (selector == ITransparentUpgradeableProxy.implementation.selector) {
ret = _dispatchImplementation();
} else {
revert('TransparentUpgradeableProxy: admin cannot fallback to proxy target');
}
assembly {
return(add(ret, 0x20), mload(ret))
}
} else {
_fallback();
super._fallback();
}
}

/**
* @dev Returns the current admin.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`
*/
function admin() external payable ifAdmin returns (address admin_) {
function _dispatchAdmin() private returns (bytes memory) {
_requireZeroValue();
admin_ = _getAdmin();

address admin = _getAdmin();
return abi.encode(admin);
}

/**
* @dev Returns the current implementation.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`
*/
function implementation() external payable ifAdmin returns (address implementation_) {
function _dispatchImplementation() private returns (bytes memory) {
_requireZeroValue();
implementation_ = _implementation();

address implementation = _implementation();
return abi.encode(implementation);
}

/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}.
*/
function changeAdmin(address newAdmin) external payable virtual ifAdmin {
function _dispatchChangeAdmin() private returns (bytes memory) {
_requireZeroValue();

address newAdmin = abi.decode(msg.data[4:], (address));
_changeAdmin(newAdmin);

return "";
}

/**
* @dev Upgrade the implementation of the proxy.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}.
*/
function upgradeTo(address newImplementation) external payable ifAdmin {
function _dispatchUpgradeTo() private returns (bytes memory) {
_requireZeroValue();

address newImplementation = abi.decode(msg.data[4:], (address));
_upgradeToAndCall(newImplementation, bytes(""), false);

return "";
}

/**
* @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified
* by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the
* proxied contract.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}.
*/
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin {
function _dispatchUpgradeToAndCall() private returns (bytes memory) {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
_upgradeToAndCall(newImplementation, data, true);

return "";
}

/**
Expand All @@ -114,14 +144,6 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
return _getAdmin();
}

/**
* @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}.
*/
function _beforeFallback() internal virtual override {
frangio marked this conversation as resolved.
Show resolved Hide resolved
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}

/**
* @dev To keep this contract fully transparent, all `ifAdmin` functions must be payable. This helper is here to
* emulate some proxy functions being non-payable while still allowing value to pass through.
Expand Down
22 changes: 11 additions & 11 deletions test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx

describe('implementation', function () {
it('returns the current implementation address', async function () {
const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress });
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });

expect(implementation).to.be.equal(this.implementationV0);
});
Expand All @@ -55,7 +55,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
it('upgrades to the requested implementation', async function () {
await this.proxy.upgradeTo(this.implementationV1, { from });

const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress });
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
expect(implementation).to.be.equal(this.implementationV1);
});

Expand Down Expand Up @@ -103,7 +103,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
});

it('upgrades to the requested implementation', async function () {
const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress });
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
expect(implementation).to.be.equal(this.behavior.address);
});

Expand Down Expand Up @@ -168,7 +168,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
});

it('upgrades to the requested version and emits an event', async function () {
const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress });
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
expect(implementation).to.be.equal(this.behaviorV1.address);
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address });
});
Expand Down Expand Up @@ -196,7 +196,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
});

it('upgrades to the requested version and emits an event', async function () {
const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress });
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
expect(implementation).to.be.equal(this.behaviorV2.address);
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address });
});
Expand Down Expand Up @@ -227,7 +227,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
});

it('upgrades to the requested version and emits an event', async function () {
const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress });
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
expect(implementation).to.be.equal(this.behaviorV3.address);
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address });
});
Expand Down Expand Up @@ -271,7 +271,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
});

it('assigns new proxy admin', async function () {
const newProxyAdmin = await this.proxy.admin.call({ from: newAdmin });
const newProxyAdmin = await this.proxy.admin({ from: newAdmin });
expect(newProxyAdmin).to.be.equal(anotherAccount);
});

Expand Down Expand Up @@ -332,21 +332,21 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx

describe('when function names clash', function () {
it('when sender is proxy admin should run the proxy function', async function () {
const value = await this.proxy.admin.call({ from: proxyAdminAddress, value: 0 });
const value = await this.proxy.admin({ from: proxyAdminAddress, value: 0 });
expect(value).to.be.equal(proxyAdminAddress);
});

it('when sender is other should delegate to implementation', async function () {
const value = await this.proxy.admin.call({ from: anotherAccount, value: 0 });
const value = await this.proxy.admin({ from: anotherAccount, value: 0 });
expect(value).to.be.equal('0x0000000000000000000000000000000011111142');
});

it('when sender is proxy admin value should not be accepted', async function () {
await expectRevert.unspecified(this.proxy.admin.call({ from: proxyAdminAddress, value: 1 }));
await expectRevert.unspecified(this.proxy.admin({ from: proxyAdminAddress, value: 1 }));
});

it('when sender is other value should be accepted', async function () {
const value = await this.proxy.admin.call({ from: anotherAccount, value: 1 });
const value = await this.proxy.admin({ from: anotherAccount, value: 1 });
expect(value).to.be.equal('0x0000000000000000000000000000000011111142');
});
});
Expand Down
4 changes: 3 additions & 1 deletion test/proxy/transparent/TransparentUpgradeableProxy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ const shouldBehaveLikeProxy = require('../Proxy.behaviour');
const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour');

const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');
const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy');

contract('TransparentUpgradeableProxy', function (accounts) {
const [proxyAdminAddress, proxyAdminOwner] = accounts;

const createProxy = async function (logic, admin, initData, opts) {
return TransparentUpgradeableProxy.new(logic, admin, initData, opts);
const { address } = await TransparentUpgradeableProxy.new(logic, admin, initData, opts);
return ITransparentUpgradeableProxy.at(address);
};

shouldBehaveLikeProxy(createProxy, proxyAdminAddress, proxyAdminOwner);
Expand Down