Skip to content

Commit 3a017d6

Browse files
committed
fix(deploy): prevent nested ProxyAdmin ownership and enforce direct admin control
1 parent 451a695 commit 3a017d6

File tree

1 file changed

+65
-9
lines changed

1 file changed

+65
-9
lines changed

script/Common.sol

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
44
import {ProxyAdmin} from '@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol';
55
import {TransparentUpgradeableProxy} from '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
66
import {Script} from 'forge-std/Script.sol';
7+
import {console2} from 'forge-std/console2.sol';
78

89
import {DelegatedSavingCircles} from '../src/contracts/DelegatedSavingCircles.sol';
910
import {SavingCircles} from '../src/contracts/SavingCircles.sol';
@@ -16,35 +17,90 @@ import {SavingCirclesViewer} from '../src/contracts/SavingCirclesViewer.sol';
1617
* @dev This contract is intended for use in Scripts and Integration Tests
1718
*/
1819
contract Common is Script {
20+
bytes32 internal constant _ERC1967_IMPLEMENTATION_SLOT =
21+
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
22+
bytes32 internal constant _ERC1967_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
23+
bytes4 internal constant _OWNER_SELECTOR = bytes4(keccak256('owner()'));
24+
bytes4 internal constant _UPGRADE_INTERFACE_VERSION_SELECTOR = bytes4(keccak256('UPGRADE_INTERFACE_VERSION()'));
25+
26+
error InvalidAdminAddress();
27+
error InvalidAdminProxyAdmin(address admin);
28+
error ProxyAdminOwnerMismatch(address proxyAdmin, address expectedOwner, address actualOwner);
29+
error ProxyAdminNotDeployed(address proxy);
30+
error ProxyImplementationSlotMismatch(address proxy, address expectedImplementation, address actualImplementation);
31+
1932
function setUp() public virtual {}
2033

2134
function _deploySavingCircles() internal returns (SavingCircles) {
2235
return new SavingCircles();
2336
}
2437

25-
function _deployProxyAdmin(address _admin) internal returns (ProxyAdmin) {
26-
return new ProxyAdmin(_admin);
27-
}
28-
2938
function _deployTransparentProxy(
3039
address _implementation,
31-
address _proxyAdmin,
40+
address _adminOwner,
3241
bytes memory _initData
3342
) internal returns (TransparentUpgradeableProxy) {
34-
return new TransparentUpgradeableProxy(_implementation, _proxyAdmin, _initData);
43+
return new TransparentUpgradeableProxy(_implementation, _adminOwner, _initData);
3544
}
3645

3746
function _deployContracts(address _admin) internal returns (TransparentUpgradeableProxy) {
47+
_assertValidAdmin(_admin);
48+
49+
SavingCircles implementation = _deploySavingCircles();
3850
TransparentUpgradeableProxy proxy = _deployTransparentProxy(
39-
address(_deploySavingCircles()),
40-
address(_deployProxyAdmin(_admin)),
41-
abi.encodeWithSelector(SavingCircles.initialize.selector, _admin)
51+
address(implementation), _admin, abi.encodeWithSelector(SavingCircles.initialize.selector, _admin)
4252
);
4353

4454
// Deploy auxiliary contracts that reference the SavingCircles proxy
4555
new DelegatedSavingCircles(address(proxy));
4656
new SavingCirclesViewer(address(proxy));
4757

58+
address proxyAdmin = _assertDeployment(address(proxy), address(implementation), _admin);
59+
60+
console2.log('Deployer', msg.sender);
61+
console2.log('Admin', _admin);
62+
console2.log('ProxyAdmin', proxyAdmin);
63+
console2.log('Proxy', address(proxy));
64+
console2.log('Implementation', address(implementation));
65+
4866
return proxy;
4967
}
68+
69+
function _assertValidAdmin(address _admin) internal view {
70+
if (_admin == address(0)) revert InvalidAdminAddress();
71+
if (_isProxyAdmin(_admin)) revert InvalidAdminProxyAdmin(_admin);
72+
}
73+
74+
function _assertDeployment(
75+
address _proxy,
76+
address _implementation,
77+
address _expectedAdminOwner
78+
) internal view returns (address proxyAdmin) {
79+
proxyAdmin = _readAddressFromSlot(_proxy, _ERC1967_ADMIN_SLOT);
80+
if (proxyAdmin == address(0)) revert ProxyAdminNotDeployed(_proxy);
81+
82+
address actualOwner = ProxyAdmin(proxyAdmin).owner();
83+
if (actualOwner != _expectedAdminOwner) {
84+
revert ProxyAdminOwnerMismatch(proxyAdmin, _expectedAdminOwner, actualOwner);
85+
}
86+
87+
address actualImplementation = _readAddressFromSlot(_proxy, _ERC1967_IMPLEMENTATION_SLOT);
88+
if (actualImplementation != _implementation) {
89+
revert ProxyImplementationSlotMismatch(_proxy, _implementation, actualImplementation);
90+
}
91+
}
92+
93+
function _readAddressFromSlot(address _contract, bytes32 _slot) internal view returns (address) {
94+
return address(uint160(uint256(vm.load(_contract, _slot))));
95+
}
96+
97+
function _isProxyAdmin(address _candidate) internal view returns (bool) {
98+
if (_candidate.code.length == 0) return false;
99+
100+
(bool ownerCallSuccess,) = _candidate.staticcall(abi.encodeWithSelector(_OWNER_SELECTOR));
101+
if (!ownerCallSuccess) return false;
102+
103+
(bool versionCallSuccess,) = _candidate.staticcall(abi.encodeWithSelector(_UPGRADE_INTERFACE_VERSION_SELECTOR));
104+
return versionCallSuccess;
105+
}
50106
}

0 commit comments

Comments
 (0)