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

feat: Operator Access Module #52

Merged
merged 13 commits into from
Nov 18, 2024
94 changes: 94 additions & 0 deletions src/contracts/EBOAccessModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;

import {
IAccessModule,
IArbitrable,
IEBOAccessModule,
IHorizonAccountingExtension,
IHorizonStaking,
IOracle
} from 'interfaces/IEBOAccessModule.sol';

import {IModule, Module} from '@defi-wonderland/prophet-core/solidity/contracts/Module.sol';
// TODO: CHECK IF IS A MODULE, ACCESSCONTROLLER OR SOMETHING ELSE
0xJabberwock marked this conversation as resolved.
Show resolved Hide resolved

contract EBOAccessModule is IEBOAccessModule, Module {
/// @inheritdoc IEBOAccessModule
IArbitrable public immutable ARBITRABLE;
/// @inheritdoc IEBOAccessModule
IHorizonStaking public horizonStaking;
Comment on lines +18 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HorizonAccountingExtension features an immutable state variable for HorizonStaking, which is inconsistent with how it is treated here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn’t aware of IHorizonStakingExtension::HORIZON_STAKING(). I wouldn't say it's inconsistent, it could definitely lead to unexpected behavior if the wrong addresses are set during deployment.

But more importantly, if horizonStaking can be obtained via a call (staticcall?) to horizonAccountingExtension, then I propose removing the horizonStaking state variable and instead calling HorizonStakingExtension::HORIZON_STAKING every time it’s needed, which is only inside of the hasAccess function.

Would this make sense to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performing an external call ad infinitum to get an immutable variable doesn't make sense to me in terms of gas efficiency, compared to storing it once. The HorizonStaking address could be sent in the constructor or got through a single external call in it—however, this last option requires HorizonAccountingExtension to have been deployed, which might not be desirable.

Beware that HorizonStaking, HorizonStakingExtension and HorizonAccountingExtension refer to different contracts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware that HorizonStaking, HorizonStakingExtension and HorizonAccountingExtension refer to different contracts.

My bad, I mixed up the contract names.

HorizonAccountingExtension is the one that has the external view HORIZON_STAKING

however, this last option requires HorizonAccountingExtension to have been deployed, which might not be desirable.

The HorizonAccountingExtension should be already deployed when EBOAccessModule is live because it's used in hasAccess to do the isAuthorized call.

And if that isn't the case, the module also have setHorizonAccountingExtension to update its address whenever necessary.

Copy link
Contributor Author

@xorsal xorsal Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now whenever the horizonAccountingExtension is updated, it will also call HORIZON_STAKING and update the horizonStaking address.

Relevant changes in: 630ce61

/// @inheritdoc IEBOAccessModule
IHorizonAccountingExtension public horizonAccountingExtension;

/**
* @notice Constructor
* @param _oracle The address of the Oracle
* @param _arbitrable The address of the Arbitrable contract
* @param _horizonStaking The address of the Horizon Staking contract
* @param _horizonAccountingExtension The address of the Horizon Accounting Extension contract
*/
constructor(
IOracle _oracle,
IArbitrable _arbitrable,
IHorizonStaking _horizonStaking,
IHorizonAccountingExtension _horizonAccountingExtension
) Module(_oracle) {
ARBITRABLE = _arbitrable;
_setHorizonStaking(_horizonStaking);
_setHorizonAccountingExtension(_horizonAccountingExtension);
}

/// @inheritdoc IEBOAccessModule
function decodeAccessControlParameters(bytes calldata _data)
public
pure
returns (IAccessModule.AccessControlParameters memory _params)
{
_params = abi.decode(_data, (IAccessModule.AccessControlParameters));
}

/// @inheritdoc IAccessModule
function hasAccess(bytes calldata _data) external view returns (bool _hasAccess) {
IAccessModule.AccessControlParameters memory _params = decodeAccessControlParameters(_data);
_hasAccess =
horizonStaking.isAuthorized(_params.accessControl.user, address(horizonAccountingExtension), _params.sender);
0xJabberwock marked this conversation as resolved.
Show resolved Hide resolved
}

/// @inheritdoc IEBOAccessModule
function setHorizonStaking(IHorizonStaking _horizonStaking) external {
ARBITRABLE.validateArbitrator(msg.sender);
_setHorizonStaking(_horizonStaking);
}

/// @inheritdoc IEBOAccessModule
function setHorizonAccountingExtension(IHorizonAccountingExtension _horizonAccountingExtension) external {
ARBITRABLE.validateArbitrator(msg.sender);
_setHorizonAccountingExtension(_horizonAccountingExtension);
}

/// @inheritdoc IModule
function moduleName() external pure returns (string memory _moduleName) {
_moduleName = 'EBOAccessModule';
}

/**
* @notice Internal function to set the horizon staking contract.
* @param _horizonStaking The new horizon staking contract.
*/
function _setHorizonStaking(IHorizonStaking _horizonStaking) internal {
horizonStaking = _horizonStaking;

emit HorizonStakingSet(_horizonStaking);
}

/**
* @notice Internal function to set the horizon accounting extension contract.
* @param _horizonAccountingExtension The new horizon accounting extension contract.
*/
function _setHorizonAccountingExtension(IHorizonAccountingExtension _horizonAccountingExtension) internal {
horizonAccountingExtension = _horizonAccountingExtension;

emit HorizonAccountingExtensionSet(_horizonAccountingExtension);
}
}
84 changes: 84 additions & 0 deletions src/interfaces/IEBOAccessModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;

import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle.sol';
import {IAccessModule} from '@defi-wonderland/prophet-core/solidity/interfaces/modules/access/IAccessModule.sol';

import {IHorizonAccountingExtension} from 'interfaces/IHorizonAccountingExtension.sol';

import {IHorizonStaking} from 'interfaces/external/IHorizonStaking.sol';

import {IArbitrable} from 'interfaces/IArbitrable.sol';

interface IEBOAccessModule is IAccessModule {
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/

/**
* @notice Emitted when the Horizon Staking contract is set
* @param _horizonStaking The new Horizon Staking contract
*/
event HorizonStakingSet(IHorizonStaking indexed _horizonStaking);

/**
* @notice Emitted when the Horizon Accounting Extension contract is set
* @param _horizonAccountingExtension The new Horizon Accounting Extension contract
*/
event HorizonAccountingExtensionSet(IHorizonAccountingExtension indexed _horizonAccountingExtension);

/*///////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////*/

/**
* @notice The Arbitrable contract
* @return _arbitrable The Arbitrable contract
*/
function ARBITRABLE() external view returns (IArbitrable _arbitrable);

/**
* @notice The Horizon Staking contract
* @return _horizonStaking The Horizon Staking contract
*/
function horizonStaking() external view returns (IHorizonStaking _horizonStaking);

/**
* @notice The Horizon Accounting Extension contract
* @return _horizonAccountingExtension The Horizon Accounting Extension contract
*/
function horizonAccountingExtension() external view returns (IHorizonAccountingExtension _horizonAccountingExtension);

/**
* @notice Decodes the access control parameters
* @param _data The encoded access control parameters
* @return _params The decoded access control parameters
*/
function decodeAccessControlParameters(bytes calldata _data)
external
pure
returns (IAccessModule.AccessControlParameters memory _params);
0xJabberwock marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Checks if the sender has access
* @param _data The encoded access control parameters
* @return _hasAccess True if the sender has access
*/
function hasAccess(bytes calldata _data) external view returns (bool _hasAccess);
0xJabberwock marked this conversation as resolved.
Show resolved Hide resolved

/*///////////////////////////////////////////////////////////////
LOGIC
//////////////////////////////////////////////////////////////*/

/**
* @notice Sets the Horizon Staking contract
* @param _horizonStaking The new Horizon Staking contract
*/
function setHorizonStaking(IHorizonStaking _horizonStaking) external;

/**
* @notice Sets the Horizon Accounting Extension contract
* @param _horizonAccountingExtension The new Horizon Accounting Extension contract
*/
function setHorizonAccountingExtension(IHorizonAccountingExtension _horizonAccountingExtension) external;
}
9 changes: 9 additions & 0 deletions src/interfaces/external/IHorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,13 @@ interface IHorizonStaking {
* @param verifierDestination The address to transfer the verifier cut to
*/
function slash(address serviceProvider, uint256 tokens, uint256 tokensVerifier, address verifierDestination) external;

/**
* @notice Check if an operator is authorized for the caller on a specific verifier / data service.
* @param operator The address to check for auth
* @param serviceProvider The service provider on behalf of whom they're claiming to act
* @param verifier The verifier / data service on which they're claiming to act
* @return Whether the operator is authorized or not
*/
function isAuthorized(address operator, address serviceProvider, address verifier) external view returns (bool);
}
167 changes: 167 additions & 0 deletions test/unit/EBOAccessModule.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;

import {
EBOAccessModule,
IAccessModule,
IArbitrable,
IEBOAccessModule,

Check warning on line 8 in test/unit/EBOAccessModule.t.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Variable "IEBOAccessModule" is unused

Check warning on line 8 in test/unit/EBOAccessModule.t.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Variable "IEBOAccessModule" is unused
IHorizonAccountingExtension,
IHorizonStaking,
IOracle
} from 'contracts/EBOAccessModule.sol';

import 'forge-std/Test.sol';

contract EBOAccessModuleForTest is EBOAccessModule {
constructor(
IOracle _oracle,
IArbitrable _arbitrable,
IHorizonStaking _horizonStaking,
IHorizonAccountingExtension _horizonAccountingExtension
) EBOAccessModule(_oracle, _arbitrable, _horizonStaking, _horizonAccountingExtension) {}

function setHorizonStakingForTest(IHorizonStaking _horizonStaking) external {
horizonStaking = _horizonStaking;
}

function setHorizonAccountingExtensionForTest(IHorizonAccountingExtension _horizonAccountingExtension) external {
horizonAccountingExtension = _horizonAccountingExtension;
}
}

contract EBOAccessModule_Unit_BaseTest is Test {
// Contracts
EBOAccessModuleForTest public eboAccessModule;
IOracle public oracle;
IArbitrable public arbitrable;
IHorizonStaking public horizonStaking;
IHorizonAccountingExtension public horizonAccountingExtension;

// Events
event HorizonStakingSet(IHorizonStaking indexed _horizonStaking);
event HorizonAccountingExtensionSet(IHorizonAccountingExtension indexed _horizonAccountingExtension);

function setUp() public {
oracle = IOracle(makeAddr('oracle'));
arbitrable = IArbitrable(makeAddr('arbitrable'));
horizonStaking = IHorizonStaking(makeAddr('horizonStaking'));
horizonAccountingExtension = IHorizonAccountingExtension(makeAddr('horizonAccountingExtension'));

eboAccessModule = new EBOAccessModuleForTest(oracle, arbitrable, horizonStaking, horizonAccountingExtension);
}
}

contract EBOAccessModule_Unit_Constructor is EBOAccessModule_Unit_BaseTest {
struct ConstructorParams {
IOracle oracle;
IArbitrable arbitrable;
IHorizonStaking horizonStaking;
IHorizonAccountingExtension horizonAccountingExtension;
}

function test_setOracle(ConstructorParams calldata _params) public {
eboAccessModule = new EBOAccessModuleForTest(
_params.oracle, _params.arbitrable, _params.horizonStaking, _params.horizonAccountingExtension
);

assertEq(address(eboAccessModule.ORACLE()), address(_params.oracle));
}

function test_setArbitrable(ConstructorParams calldata _params) public {
eboAccessModule = new EBOAccessModuleForTest(
_params.oracle, _params.arbitrable, _params.horizonStaking, _params.horizonAccountingExtension
);

assertEq(address(eboAccessModule.ARBITRABLE()), address(_params.arbitrable));
}

function test_setHorizonStaking(ConstructorParams calldata _params) public {
vm.expectEmit();
emit HorizonStakingSet(_params.horizonStaking);

eboAccessModule = new EBOAccessModuleForTest(
_params.oracle, _params.arbitrable, _params.horizonStaking, _params.horizonAccountingExtension
);

assertEq(address(eboAccessModule.horizonStaking()), address(_params.horizonStaking));
}

function test_setHorizonAccountingExtension(ConstructorParams calldata _params) public {
vm.expectEmit();
emit HorizonAccountingExtensionSet(_params.horizonAccountingExtension);

eboAccessModule = new EBOAccessModuleForTest(
_params.oracle, _params.arbitrable, _params.horizonStaking, _params.horizonAccountingExtension
);

assertEq(address(eboAccessModule.horizonAccountingExtension()), address(_params.horizonAccountingExtension));
}
}

contract EBOAccessModule_Unit_ModuleName is EBOAccessModule_Unit_BaseTest {
function test_returnModuleName() public view {
assertEq(eboAccessModule.moduleName(), 'EBOAccessModule');
}
}

contract EBOAccessModule_Unit_DecodeAccessControlParameters is EBOAccessModule_Unit_BaseTest {
function test_decodeAccessControlParameters(IAccessModule.AccessControlParameters calldata _params) public view {
bytes memory _data = abi.encode(_params);

IAccessModule.AccessControlParameters memory _decodedParams = eboAccessModule.decodeAccessControlParameters(_data);

assertEq(_decodedParams.accessControl.user, _params.accessControl.user);
assertEq(address(_decodedParams.sender), address(_params.sender));
assertEq(_decodedParams.typehash, _params.typehash);
}
}

contract EBOAccessModule_Unit_HasAccess is EBOAccessModule_Unit_BaseTest {
function test_hasAccess(IAccessModule.AccessControlParameters calldata _params) public {
bytes memory _data = abi.encode(_params);

vm.mockCall(
address(horizonStaking),
abi.encodeWithSelector(
horizonStaking.isAuthorized.selector,
_params.accessControl.user,
address(horizonAccountingExtension),
_params.sender
),
abi.encode(true)
);

eboAccessModule.hasAccess(_data);
}
}

contract EBOAccessModule_Unit_SetHorizonStaking is EBOAccessModule_Unit_BaseTest {
function test_setHorizonStaking(IHorizonStaking _horizonStaking) public {
vm.mockCall(
address(arbitrable), abi.encodeWithSelector(arbitrable.validateArbitrator.selector, msg.sender), abi.encode(true)
);

vm.expectEmit();
emit HorizonStakingSet(_horizonStaking);

eboAccessModule.setHorizonStaking(_horizonStaking);

assertEq(address(eboAccessModule.horizonStaking()), address(_horizonStaking));
}
}

contract EBOAccessModule_Unit_SetHorizonAccountingExtension is EBOAccessModule_Unit_BaseTest {
function test_setHorizonAccountingExtension(IHorizonAccountingExtension _horizonAccountingExtension) public {
vm.mockCall(
address(arbitrable), abi.encodeWithSelector(arbitrable.validateArbitrator.selector, msg.sender), abi.encode(true)
);

vm.expectEmit();
emit HorizonAccountingExtensionSet(_horizonAccountingExtension);

eboAccessModule.setHorizonAccountingExtension(_horizonAccountingExtension);

assertEq(address(eboAccessModule.horizonAccountingExtension()), address(_horizonAccountingExtension));
}
}
2 changes: 1 addition & 1 deletion test/unit/EBORequestCreator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ contract EBORequestCreatorForTest is EBORequestCreator {
}
}

abstract contract EBORequestCreator_Unit_BaseTest is Test {
contract EBORequestCreator_Unit_BaseTest is Test {
/// Events
event RequestCreated(
bytes32 indexed _requestId, IOracle.Request _request, uint256 indexed _epoch, string indexed _chainId
Expand Down
Loading