Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/npm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- uses: actions/setup-node@v3
with:
node-version: "20"
node-version: "22"
cache: "npm"

- run: npm ci
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
node-version: "22"
cache: "npm"
- run: npm ci
- run: node scripts/generateAMPSkips.js 1 5 10 24
- run: npx hardhat compile
- run: npx hardhat size-contracts
- run: npm run solhint
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ node_modules
# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/chain-31337

/verifiable-binaries
/contracts-exposed
soljson-latest.js

# AMP generated files
contracts/amps/*.sol
4 changes: 3 additions & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"plugins": ["prettier-plugin-solidity"],
"plugins": [
"prettier-plugin-solidity"
],
"overrides": [
{
"files": "*.sol",
Expand Down
1 change: 1 addition & 0 deletions .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"prettier/prettier": "error",
"payable-fallback": "off",
"avoid-throw": "off",
"use-natspec": "off",
"avoid-suicide": "error",
"avoid-sha3": "warn",
"compiler-version": [
Expand Down
1 change: 1 addition & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
contracts/dependencies/*
contracts/mock/*
contracts/amps/*
20 changes: 18 additions & 2 deletions contracts/AccessManagedProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,24 @@ contract AccessManagedProxy is ERC1967Proxy {
* This function does not return to its internal call site, it will return directly to the external caller.
*/
function _delegate(address implementation) internal virtual override {
(bool immediate, ) = ACCESS_MANAGER.canCall(msg.sender, address(this), bytes4(msg.data[0:4]));
if (!immediate) revert AccessManagedUnauthorized(msg.sender);
bytes4 selector = bytes4(msg.data[0:4]);
bool immediate = _skipAC(selector); // reuse immediate variable both for skipped methods and canCall result
if (!immediate) {
(immediate, ) = ACCESS_MANAGER.canCall(msg.sender, address(this), selector);
if (!immediate) revert AccessManagedUnauthorized(msg.sender);
}
super._delegate(implementation);
}

/**
* @notice Returns whether to skip the access control validation or not
* @dev Hook called before ACCESS_MANAGER.canCall to enable skipping the call to the access manager for performance
* reasons (for example on views) or to remove access control for other specific cases
* @param selector The selector of the method called
* @return Whether the access control using ACCESS_MANAGER should be skipped or not
*/
// solhint-disable-next-line no-unused-vars
function _skipAC(bytes4 selector) internal view virtual returns (bool) {
return false;
}
}
36 changes: 26 additions & 10 deletions contracts/mock/DummyImplementation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ pragma solidity ^0.8.16;

import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

interface IDummy {
function method1() external;
function method2() external;
}

/**
* @title Dummy implementation contract that supports upgrade and logs methods called
*
* @custom:security-contact [email protected]
* @author Ensuro
*/
contract DummyImplementation is UUPSUpgradeable, IDummy {
contract DummyImplementation is UUPSUpgradeable {
event MethodCalled(bytes4 selector);

/// @custom:oz-upgrades-unsafe-allow constructor
Expand All @@ -27,11 +22,32 @@ contract DummyImplementation is UUPSUpgradeable, IDummy {
/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address newImplementation) internal override {}

function method1() external override {
emit MethodCalled(IDummy.method1.selector);
// For making gas usage comparisons easier, I'm going to use different methods for each variant
function callThruAMP() external {
emit MethodCalled(this.callThruAMP.selector);
}

function callThru1967() external {
emit MethodCalled(this.callThru1967.selector);
}

function callDirect() external {
emit MethodCalled(this.callDirect.selector);
}

function callThruAMPSkippedMethod() external {
emit MethodCalled(this.callThruAMPSkippedMethod.selector);
}

function callThruAMPNonSkippedMethod() external {
emit MethodCalled(this.callThruAMPNonSkippedMethod.selector);
}

function viewMethod() external view returns (address) {
return msg.sender;
}

function method2() external override {
emit MethodCalled(IDummy.method2.selector);
function pureMethod() external pure returns (uint256) {
return 123456;
}
}
15 changes: 12 additions & 3 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require("@openzeppelin/hardhat-upgrades");
require("hardhat-dependency-compiler");
require("hardhat-contract-sizer");
require("hardhat-ignore-warnings");
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
Expand All @@ -21,8 +22,16 @@ module.exports = {
disambiguatePaths: false,
},
dependencyCompiler: {
paths: [
"@openzeppelin/contracts/access/manager/AccessManager.sol",
],
paths: ["@openzeppelin/contracts/access/manager/AccessManager.sol"],
},
warnings: {
"contracts/AccessManagedProxy.sol": {
"missing-receive": "off",
"unused-param": "off",
},
"contracts/amps/AccessManagedProxyS*.sol": {
"missing-receive": "off",
"unused-param": "off",
},
},
};
57 changes: 57 additions & 0 deletions js/deployProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const hre = require("hardhat");
const { ethers } = hre;
const { deploy: ozUpgradesDeploy } = require("@openzeppelin/hardhat-upgrades/dist/utils");

/**
* Deploys a contract using an AccessManagedProxy. Similar to hre.upgrades.deployProxy, but using AccessManagedProxy
*
* @param {contractFactory} The contract factory of the implementation contract
* @param {initializeArgs} Arguments for `initialize`
* @param {opts} Options for hre.upgrades.deployProxy with some AccessManagedProxy additions:
* - skipViewsAndPure: if true, deploys a proxy that will skip the access control for all the view and
* pure methods
* - skipMethods: list of method names that will skip the access control. Added to views and pure, if
* skipViewsAndPure is true.
* - acMgr: mandatory argument that will be used for the AMP
* @returns {contract} Promise<Contract>
*/
async function deployAMPProxy(contractFactory, initializeArgs = [], opts = {}) {
const { acMgr, skipViewsAndPure, skipMethods } = opts;
if (acMgr === undefined) throw new Error("Missing required `acMgr` in opts");
let skipSelectors = [];
if (skipViewsAndPure) {
skipSelectors = contractFactory.interface.fragments
.filter(
(fragment) =>
fragment.type === "function" && (fragment.stateMutability === "pure" || fragment.stateMutability === "view")
)
.map((fragment) => fragment.selector);
}
if (skipMethods !== undefined && skipMethods.length > 0) {
skipSelectors.push(
...skipMethods.map((method) =>
method.startsWith("0x") ? method : contractFactory.interface.getFunction(method).selector
)
);
}
let proxyFactory, deployFunction;
if (skipSelectors.length > 0) {
proxyFactory = await ethers.getContractFactory(`AccessManagedProxyS${skipSelectors.length}`);
deployFunction = async (hre_, opts, factory, ...args) =>
ozUpgradesDeploy(hre_, opts, factory, ...args, acMgr, skipSelectors);
} else {
proxyFactory = await ethers.getContractFactory("AccessManagedProxy");
deployFunction = async (hre_, opts, factory, ...args) => ozUpgradesDeploy(hre_, opts, factory, ...args, acMgr);
}

return hre.upgrades.deployProxy(contractFactory, [], {
...opts,
kind: "uups",
proxyFactory,
deployFunction,
});
}

module.exports = {
deployAMPProxy,
};
Loading
Loading