diff --git a/.gitmodules b/.gitmodules index 888d42d..178960b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/foundry.toml b/foundry.toml index aff83dc..9c2a1e3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,43 +3,41 @@ src = 'src' tests = 'tests' out = 'out' libs = ['lib'] -solc = "0.8.14" -remappings = [ -] +remappings = [] # See more config options https://github.com/gakonst/foundry/tree/master/config [rpc_endpoints] -ethereum="${RPC_MAINNET}" -ethereum-testnet="${RPC_MAINNET_TESTNET}" -polygon="${RPC_POLYGON}" -polygon-testnet="${RPC_POLYGON_TESTNET}" -arbitrum="${RPC_ARBITRUM}" -arbitrum-testnet="${RPC_ARBITRUM_TESTNET}" -metis="${RPC_METIS}" -metis-testnet="${RPC_METIS_TESTNET}" -avalanche="${RPC_AVALANCHE}" -avalanche-testnet="${RPC_AVALANCHE_TESTNET}" -optimism="${RPC_OPTIMISM}" -optimism-testnet="${RPC_OPTIMISM_TESTNET}" -fantom="${RPC_FANTOM}" -fantom-testnet="${RPC_FANTOM_TESTNET}" -binance="${RPC_BINANCE}" -binance-testnet="${RPC_BINANCE_TESTNET}" +ethereum = "${RPC_MAINNET}" +ethereum-testnet = "${RPC_MAINNET_TESTNET}" +polygon = "${RPC_POLYGON}" +polygon-testnet = "${RPC_POLYGON_TESTNET}" +arbitrum = "${RPC_ARBITRUM}" +arbitrum-testnet = "${RPC_ARBITRUM_TESTNET}" +metis = "${RPC_METIS}" +metis-testnet = "${RPC_METIS_TESTNET}" +avalanche = "${RPC_AVALANCHE}" +avalanche-testnet = "${RPC_AVALANCHE_TESTNET}" +optimism = "${RPC_OPTIMISM}" +optimism-testnet = "${RPC_OPTIMISM_TESTNET}" +fantom = "${RPC_FANTOM}" +fantom-testnet = "${RPC_FANTOM_TESTNET}" +binance = "${RPC_BINANCE}" +binance-testnet = "${RPC_BINANCE_TESTNET}" [etherscan] -ethereum={key="${ETHERSCAN_API_KEY_MAINNET}", chain=1 } -ethereum-testnet={key="${ETHERSCAN_API_KEY_MAINNET}",chain=1} -optimism={key="${ETHERSCAN_API_KEY_OPTIMISM}",chain=10} -optimism-testnet={key="${ETHERSCAN_API_KEY_OPTIMISM}",chain=10} -avalanche={key="${ETHERSCAN_API_KEY_AVALANCHE}",chain=43114} -avalanche-testnet={key="${ETHERSCAN_API_KEY_AVALANCHE}",chain=43114} -polygon={key="${ETHERSCAN_API_KEY_POLYGON}",chain=137} -polygon-testnet={key="${ETHERSCAN_API_KEY_POLYGON}",chain=137} -arbitrum={key="${ETHERSCAN_API_KEY_ARBITRUM}",chain=42161} -arbitrum-testnet={key="${ETHERSCAN_API_KEY_ARBITRUM}",chain=42161} -metis={ key="any", chain=1088, url='https://andromeda-explorer.metis.io/' } -metis-testnet={ key="any", chain=599, url='https://goerli.explorer.metisdevops.link/' } -fantom={key="${ETHERSCAN_API_KEY_FANTOM}",chain=250} -fantom-testnet={key="${ETHERSCAN_API_KEY_FANTOM}",chain=250} -binance={key="${ETHERSCAN_API_KEY_BINANCE}",chain=56} -binance-testnet={key="${ETHERSCAN_API_KEY_BINANCE}",chain=56} +ethereum = { key = "${ETHERSCAN_API_KEY_MAINNET}", chain = 1 } +ethereum-testnet = { key = "${ETHERSCAN_API_KEY_MAINNET}", chain = 1 } +optimism = { key = "${ETHERSCAN_API_KEY_OPTIMISM}", chain = 10 } +optimism-testnet = { key = "${ETHERSCAN_API_KEY_OPTIMISM}", chain = 10 } +avalanche = { key = "${ETHERSCAN_API_KEY_AVALANCHE}", chain = 43114 } +avalanche-testnet = { key = "${ETHERSCAN_API_KEY_AVALANCHE}", chain = 43114 } +polygon = { key = "${ETHERSCAN_API_KEY_POLYGON}", chain = 137 } +polygon-testnet = { key = "${ETHERSCAN_API_KEY_POLYGON}", chain = 137 } +arbitrum = { key = "${ETHERSCAN_API_KEY_ARBITRUM}", chain = 42161 } +arbitrum-testnet = { key = "${ETHERSCAN_API_KEY_ARBITRUM}", chain = 42161 } +metis = { key = "any", chain = 1088, url = 'https://andromeda-explorer.metis.io/' } +metis-testnet = { key = "any", chain = 599, url = 'https://goerli.explorer.metisdevops.link/' } +fantom = { key = "${ETHERSCAN_API_KEY_FANTOM}", chain = 250 } +fantom-testnet = { key = "${ETHERSCAN_API_KEY_FANTOM}", chain = 250 } +binance = { key = "${ETHERSCAN_API_KEY_BINANCE}", chain = 56 } +binance-testnet = { key = "${ETHERSCAN_API_KEY_BINANCE}", chain = 56 } diff --git a/lib/forge-std b/lib/forge-std index 4a79aca..3d8086d 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 4a79aca83f8075f8b1b4fe9153945fef08375630 +Subproject commit 3d8086d4911b36c1874531ce8c367e6cfd028e80 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..723f8ca --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 diff --git a/src/contracts/access-control/UpgradableOwnableWithGuardian.sol b/src/contracts/access-control/UpgradableOwnableWithGuardian.sol new file mode 100644 index 0000000..0b4eb1b --- /dev/null +++ b/src/contracts/access-control/UpgradableOwnableWithGuardian.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol'; +import {IWithGuardian} from './interfaces/IWithGuardian.sol'; + +/** + * Forked version of https://github.com/bgd-labs/solidity-utils/blob/main/src/contracts/access-control/OwnableWithGuardian.sol + * Relying on UpgradableOwnable & moving the storage to 7201 + */ +abstract contract UpgradableOwnableWithGuardian is OwnableUpgradeable, IWithGuardian { + /// @custom:storage-location erc7201:aave.storage.OwnableWithGuardian + struct OwnableWithGuardian { + address _guardian; + } + + // keccak256(abi.encode(uint256(keccak256("aave.storage.OwnableWithGuardian")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant OwnableWithGuardianStorageLocation = + 0xdc8016945fab92f4608d8f23802ef36d865b35bd839402e24dec05cd76049e00; + + function _getOwnableWithGuardianStorage() private pure returns (OwnableWithGuardian storage $) { + assembly { + $.slot := OwnableWithGuardianStorageLocation + } + } + + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OnlyGuardianInvalidCaller(address account); + + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OnlyGuardianOrOwnerInvalidCaller(address account); + + /** + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. + */ + function __Ownable_With_Guardian_init(address initialGuardian) internal onlyInitializing { + _updateGuardian(initialGuardian); + } + + modifier onlyGuardian() { + _checkGuardian(); + _; + } + + modifier onlyOwnerOrGuardian() { + _checkOwnerOrGuardian(); + _; + } + + function guardian() public view override returns (address) { + OwnableWithGuardian storage $ = _getOwnableWithGuardianStorage(); + return $._guardian; + } + + /// @inheritdoc IWithGuardian + function updateGuardian(address newGuardian) external override onlyOwnerOrGuardian { + _updateGuardian(newGuardian); + } + + /** + * @dev method to update the guardian + * @param newGuardian the new guardian address + */ + function _updateGuardian(address newGuardian) internal { + OwnableWithGuardian storage $ = _getOwnableWithGuardianStorage(); + address oldGuardian = $._guardian; + $._guardian = newGuardian; + emit GuardianUpdated(oldGuardian, newGuardian); + } + + function _checkGuardian() internal view { + if (guardian() != _msgSender()) revert OnlyGuardianInvalidCaller(_msgSender()); + } + + function _checkOwnerOrGuardian() internal view { + if (_msgSender() != owner() && _msgSender() != guardian()) + revert OnlyGuardianOrOwnerInvalidCaller(_msgSender()); + } +} diff --git a/test/UpgradableOwnableWithGuardian.t copy.sol b/test/UpgradableOwnableWithGuardian.t copy.sol new file mode 100644 index 0000000..83e1532 --- /dev/null +++ b/test/UpgradableOwnableWithGuardian.t copy.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import 'forge-std/Test.sol'; +import {UpgradableOwnableWithGuardian} from '../src/contracts/access-control/UpgradableOwnableWithGuardian.sol'; + +contract ImplOwnableWithGuardian is UpgradableOwnableWithGuardian { + function initialize(address owner, address guardian) public initializer { + __Ownable_init(owner); + __Ownable_With_Guardian_init(guardian); + } + + function mock_onlyGuardian() external onlyGuardian {} + + function mock_onlyOwnerOrGuardian() external onlyOwnerOrGuardian {} +} + +contract TestOfUpgradableOwnableWithGuardian is Test { + UpgradableOwnableWithGuardian public withGuardian; + + address owner = address(0x4); + address guardian = address(0x8); + + function setUp() public { + withGuardian = new ImplOwnableWithGuardian(); + ImplOwnableWithGuardian(address(withGuardian)).initialize(owner, guardian); + } + + function test_initializer() external { + assertEq(withGuardian.owner(), owner); + assertEq(withGuardian.guardian(), guardian); + } + + function test_onlyGuardian() external { + vm.expectRevert( + abi.encodeWithSelector( + UpgradableOwnableWithGuardian.OnlyGuardianInvalidCaller.selector, + address(this) + ) + ); + ImplOwnableWithGuardian(address(withGuardian)).mock_onlyGuardian(); + } + + function test_onlyOwnerOrGuardian() external { + vm.expectRevert( + abi.encodeWithSelector( + UpgradableOwnableWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, + address(this) + ) + ); + ImplOwnableWithGuardian(address(withGuardian)).mock_onlyOwnerOrGuardian(); + } + + function test_updateGuardian_guardian(address newGuardian) external { + vm.prank(guardian); + withGuardian.updateGuardian(newGuardian); + } + + function test_updateGuardian_owner(address newGuardian) external { + vm.prank(owner); + withGuardian.updateGuardian(newGuardian); + } + + function test_updateGuardian_eoa(address eoa, address newGuardian) external { + vm.assume(eoa != owner && eoa != guardian); + + vm.prank(eoa); + vm.expectRevert( + abi.encodeWithSelector( + UpgradableOwnableWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, + eoa + ) + ); + withGuardian.updateGuardian(newGuardian); + } +}