diff --git a/contracts/contracts/coordination/GlobalAllowList.sol b/contracts/contracts/coordination/GlobalAllowList.sol index 0fbc2926b..0f4768e20 100644 --- a/contracts/contracts/coordination/GlobalAllowList.sol +++ b/contracts/contracts/coordination/GlobalAllowList.sol @@ -11,7 +11,7 @@ import "./Coordinator.sol"; /** * @title GlobalAllowList - * @notice Manages a global allow list of addresses that are authorized to decrypt ciphertexts. + * @notice Manages a global allow list of addresses that are authorized to decrypt plaintexts. */ contract GlobalAllowList is IEncryptionAuthorizer, Initializable { using MessageHashUtils for bytes32; @@ -129,7 +129,7 @@ contract GlobalAllowList is IEncryptionAuthorizer, Initializable { function authorize( uint32 ritualId, address[] calldata addresses - ) external canSetAuthorizations(ritualId) { + ) external virtual canSetAuthorizations(ritualId) { setAuthorizations(ritualId, addresses, true); } @@ -141,7 +141,7 @@ contract GlobalAllowList is IEncryptionAuthorizer, Initializable { function deauthorize( uint32 ritualId, address[] calldata addresses - ) external canSetAuthorizations(ritualId) { + ) external virtual canSetAuthorizations(ritualId) { setAuthorizations(ritualId, addresses, false); } diff --git a/contracts/contracts/coordination/ManagedAllowList.sol b/contracts/contracts/coordination/ManagedAllowList.sol index 1d06f0777..6f52784f5 100644 --- a/contracts/contracts/coordination/ManagedAllowList.sol +++ b/contracts/contracts/coordination/ManagedAllowList.sol @@ -2,149 +2,119 @@ pragma solidity ^0.8.0; -import "../lib/LookupKey.sol"; +import "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import "./GlobalAllowList.sol"; -import "./Coordinator.sol"; -import {UpfrontSubscriptionWithEncryptorsCap} from "./subscription/Subscription.sol"; /** * @title ManagedAllowList - * @notice Manages a list of addresses that are authorized to decrypt ciphertexts, with additional management features. + * @notice Manages a list of addresses that are authorized to encrypt plaintexts, with additional management features. * This contract extends the GlobalAllowList contract and introduces additional management features. - * It maintains a reference to a Subscription contract, which is used to manage the authorization caps for different addresses and rituals. - * The Subscription contract is used to enforce limits on the number of authorization actions that can be performed, and these limits can be set and updated through the ManagedAllowList contract. */ -contract ManagedAllowList is GlobalAllowList { - mapping(bytes32 => uint256) internal allowance; +contract ManagedAllowList is GlobalAllowList, AccessControlUpgradeable { + mapping(address authAdmin => mapping(bytes32 lookupKey => bool)) public authAdmins; - /** - * @notice The Subscription contract used to manage authorization caps - */ - // TODO: Should it be updatable? - UpfrontSubscriptionWithEncryptorsCap public immutable subscription; + bytes32 public constant COHORT_ADMIN_BASE = keccak256("COHORT_ADMIN"); + bytes32 public constant AUTH_ADMIN_BASE = keccak256("AUTH_ADMIN"); /** - * @notice Emitted when an administrator cap is set - * @param ritualId The ID of the ritual - * @param _address The address of the administrator - * @param cap The cap value + * @notice Sets the coordinator contract + * @dev The coordinator contract cannot be a zero address and must have a valid number of rituals + * @param _coordinator The address of the coordinator contract */ - event AdministratorCapSet(uint32 indexed ritualId, address indexed _address, uint256 cap); + constructor(Coordinator _coordinator) GlobalAllowList(_coordinator) {} /** - * @notice Sets the coordinator and subscription contracts - * @dev The coordinator and subscription contracts cannot be zero addresses - * @param _coordinator The address of the coordinator contract - * @param _subscription The address of the subscription contract + * Prepares role ID for the specific ritual + * @param ritualId The ID of the ritual + * @param role Role ID */ - constructor( - Coordinator _coordinator, - UpfrontSubscriptionWithEncryptorsCap _subscription // TODO replace with IFeeModel subscription - ) GlobalAllowList(_coordinator) { - require(address(_subscription) != address(0), "Subscription cannot be the zero address"); - subscription = _subscription; + function ritualRole(uint32 ritualId, bytes32 role) public pure returns (bytes32) { + return keccak256(abi.encodePacked(ritualId, role)); } /** - * @notice Checks if the sender is the authority of the ritual + * Prepares cohort admin role ID for the specific ritual * @param ritualId The ID of the ritual */ - modifier onlyCohortAuthority(uint32 ritualId) { - require( - coordinator.getAuthority(ritualId) == msg.sender, - "Only cohort authority is permitted" - ); - _; + function cohortAdminRole(uint32 ritualId) public pure returns (bytes32) { + return ritualRole(ritualId, COHORT_ADMIN_BASE); } /** - * @notice Checks if the sender has allowance to set authorizations - * @dev This function overrides the canSetAuthorizations modifier in the GlobalAllowList contract + * Prepares auth admin role ID for the specific ritual * @param ritualId The ID of the ritual */ - modifier canSetAuthorizations(uint32 ritualId) override { - require(getAllowance(ritualId, msg.sender) > 0, "Only administrator is permitted"); - _; + function authAdminRole(uint32 ritualId) public pure returns (bytes32) { + return ritualRole(ritualId, AUTH_ADMIN_BASE); } /** - * @notice Returns the allowance of an administrator for a ritual + * Acquire cohort admin role * @param ritualId The ID of the ritual - * @param admin The address of the administrator - * @return The allowance of the administrator */ - function getAllowance(uint32 ritualId, address admin) public view returns (uint256) { - return allowance[LookupKey.lookupKey(ritualId, admin)]; + function initializeCohortAdminRole(uint32 ritualId) public { + bytes32 cohortAdminRole = cohortAdminRole(ritualId); + address authority = coordinator.getAuthority(ritualId); + require(authority != address(0), "Ritual is not initiated"); + if (hasRole(cohortAdminRole, authority)) { + return; + } + _setRoleAdmin(authAdminRole(ritualId), cohortAdminRole); + _grantRole(cohortAdminRole, authority); } /** - * @dev This function is called before the setAuthorizations function + * Grants Auth Admin role. Can be called only by Cohort Admin or Authority of the ritual + * @dev Automatically grants Cohort Admin role to the authority if was not granted before * @param ritualId The ID of the ritual - * @param addresses The addresses to be authorized - * @param value The authorization status + * @param account Address for Auth Admin role */ - function _beforeSetAuthorization( - uint32 ritualId, - address[] calldata addresses, - // TODO: Currently unused, remove? - bool value - ) internal override { - super._beforeSetAuthorization(ritualId, addresses, value); - for (uint256 i = 0; i < addresses.length; i++) { - require( - authActions[ritualId] < - subscription.authorizationActionsCap(ritualId, addresses[i]), - "Authorization cap exceeded" - ); - } + function grantAuthAdminRole(uint32 ritualId, address account) external { + initializeCohortAdminRole(ritualId); + grantRole(authAdminRole(ritualId), account); } /** - * @notice Sets the administrator caps for a ritual - * @dev Only active rituals can set administrator caps + * @notice Checks if the sender is an Authorization Admin of the ritual * @param ritualId The ID of the ritual - * @param addresses The addresses of the administrators - * @param value The cap value */ - function setAdministratorCaps( - uint32 ritualId, - address[] calldata addresses, - uint256 value - ) internal { - require( - coordinator.isRitualActive(ritualId), - "Only active rituals can set administrator caps" - ); - for (uint256 i = 0; i < addresses.length; i++) { - allowance[LookupKey.lookupKey(ritualId, addresses[i])] = value; - emit AdministratorCapSet(ritualId, addresses[i], value); - } - authActions[ritualId] += addresses.length; + modifier canSetAuthorizations(uint32 ritualId) virtual override { + require(hasRole(authAdminRole(ritualId), msg.sender), "Only auth admin is permitted"); + _; } /** - * @notice Adds administrators for a ritual + * @notice Authorizes a list of addresses for a ritual * @param ritualId The ID of the ritual - * @param addresses The addresses of the administrators - * @param cap The cap value + * @param addresses The addresses to be authorized */ - function addAdministrators( + function authorize( uint32 ritualId, - address[] calldata addresses, - uint256 cap - ) external onlyCohortAuthority(ritualId) { - setAdministratorCaps(ritualId, addresses, cap); + address[] calldata addresses + ) external override canSetAuthorizations(ritualId) { + setAuthorizations(ritualId, addresses, true); + for (uint256 i = 0; i < addresses.length; i++) { + bytes32 lookupKey = LookupKey.lookupKey(ritualId, addresses[i]); + authAdmins[msg.sender][lookupKey] = true; + } } /** - * @notice Removes administrators for a ritual + * @notice Deauthorizes a list of addresses for a ritual * @param ritualId The ID of the ritual - * @param addresses The addresses of the administrators + * @param addresses The addresses to be deauthorized */ - function removeAdministrators( + function deauthorize( uint32 ritualId, address[] calldata addresses - ) external onlyCohortAuthority(ritualId) { - setAdministratorCaps(ritualId, addresses, 0); + ) external override canSetAuthorizations(ritualId) { + for (uint256 i = 0; i < addresses.length; i++) { + bytes32 lookupKey = LookupKey.lookupKey(ritualId, addresses[i]); + require( + authAdmins[msg.sender][lookupKey], + "Encryptor has not been previously authorized by the sender" + ); + } + setAuthorizations(ritualId, addresses, false); } } diff --git a/contracts/contracts/coordination/subscription/Subscription.sol b/contracts/contracts/coordination/subscription/Subscription.sol deleted file mode 100644 index 7111a7d93..000000000 --- a/contracts/contracts/coordination/subscription/Subscription.sol +++ /dev/null @@ -1,279 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "../../lib/LookupKey.sol"; -import "../Coordinator.sol"; - -using SafeERC20 for IERC20; - -/** - * @title Subscription - * @notice Manages the subscription information for rituals. - * @dev This contract is abstract and should be extended by a concrete implementation. - * It maintains a reference to a Coordinator contract and a fee token (ERC20). - * Each subscription has an associated SubscriptionInfo struct which keeps track of the subscription details. - */ -abstract contract Subscription { - struct SubscriptionInfo { - uint256 paidFor; - uint256 spent; - uint256 expiration; - address subscriber; - } - - Coordinator public immutable coordinator; - IERC20 public immutable feeToken; - - // Mapping from subscription ID to subscription info - mapping(uint32 => SubscriptionInfo) public subscriptions; - - // Mapping from (ritualId, address) to subscription ID - mapping(bytes32 => uint32) public subscribers; - - uint32 public numberOfSubscriptions; - - // TODO: DAO Treasury - // TODO: Should it be updatable? - address public immutable beneficiary; - - /** - * @notice Emitted when a subscription is created - * @param subscriptionId The ID of the subscription - * @param subscriber The address of the subscriber - * @param ritualId The ID of the ritual - */ - event SubscriptionCreated( - uint32 indexed subscriptionId, - address indexed subscriber, - uint32 indexed ritualId - ); - - /** - * @notice Emitted when a subscription is cancelled - * @param subscriptionId The ID of the subscription - * @param subscriber The address of the subscriber - * @param ritualId The ID of the ritual - */ - event SubscriptionCancelled( - uint32 indexed subscriptionId, - address indexed subscriber, - uint32 indexed ritualId - ); - - /** - * @notice Emitted when a subscription is paid - * @param subscriptionId The ID of the subscription - * @param subscriber The address of the subscriber - * @param amount The amount paid - */ - event SubscriptionPaid( - uint32 indexed subscriptionId, - address indexed subscriber, - uint256 amount - ); - - /** - * @notice Emitted when a subscription is spent - * @param beneficiary The address of the beneficiary - * @param amount The amount withdrawn - * @param amount The amount spent - */ - event WithdrawalToBeneficiary(address indexed beneficiary, uint256 amount); - - /** - * @notice Sets the coordinator and fee token contracts - * @dev The coordinator and fee token contracts cannot be zero addresses - * @param _coordinator The address of the coordinator contract - * @param _feeToken The address of the fee token contract - * @param _beneficiary The address of the beneficiary - */ - constructor(Coordinator _coordinator, IERC20 _feeToken, address _beneficiary) { - require(address(_coordinator) != address(0), "Coordinator cannot be the zero address"); - require(address(_feeToken) != address(0), "Fee token cannot be the zero address"); - require(_beneficiary != address(0), "Beneficiary cannot be the zero address"); - coordinator = _coordinator; - feeToken = _feeToken; - beneficiary = _beneficiary; - } - - /** - * @notice Returns the subscription fee - * @return The subscription fee - */ - function subscriptionFee() public pure returns (uint256) { - return 42 * 10 ** 20; // TODO - } - - /** - * @notice Returns the base expiration duration - * @return The base expiration duration - */ - function baseExpiration() public pure returns (uint256) { - return 52 weeks; // TODO - } - - /** - * @notice Creates a new subscription - * @param ritualId The ID of the ritual - */ - function newSubscription(uint32 ritualId) external { - uint32 subscriptionId = numberOfSubscriptions; - SubscriptionInfo storage sub = subscriptions[subscriptionId]; - sub.subscriber = msg.sender; - paySubscriptionFor(subscriptionId); - - subscribers[LookupKey.lookupKey(ritualId, msg.sender)] = subscriptionId; - numberOfSubscriptions += 1; - - emit SubscriptionCreated(subscriptionId, msg.sender, ritualId); - } - - /** - * @notice Pays for a subscription - * @param subscriptionId The ID of the subscription - */ - function paySubscriptionFor(uint32 subscriptionId) public virtual { - uint256 amount = subscriptionFee(); - - SubscriptionInfo storage sub = subscriptions[subscriptionId]; - sub.paidFor += amount; - sub.expiration += baseExpiration(); - - feeToken.safeTransferFrom(msg.sender, address(this), amount); - - // TODO: We already emit SubscriptionCreated, do we need this? - emit SubscriptionPaid(subscriptionId, msg.sender, amount); - } - - /** - * @notice Checks if a spender can spend from a subscription - * @param subscriptionId The ID of the subscription - * @param spender The address of the spender - * @return True if the spender can spend from the subscription, false otherwise - */ - function canSpendFromSubscription( - // TODO: Currently unused, remove? - // solhint-disable-next-line no-unused-vars - uint32 subscriptionId, - address spender - ) public returns (bool) { - // By default, only coordinator can spend from subscription - return spender == address(coordinator); - } - - /** - * @notice Spends from a subscription - * @param subscriptionId The ID of the subscription - * @param amount The amount to spend - */ - function spendFromSubscription(uint32 subscriptionId, uint256 amount) external { - require(canSpendFromSubscription(subscriptionId, msg.sender), "Unauthorized spender"); - feeToken.safeTransferFrom(address(this), msg.sender, amount); - } - - /** - * @notice Calculates the available fees that can be withdrawn by the beneficiary - * @return The available amount of fees - */ - function calculateAvailableAmountForBeneficiary() public view returns (uint256) { - uint256 availableAmount = 0; - for (uint32 i = 0; i < numberOfSubscriptions; i++) { - SubscriptionInfo storage sub = subscriptions[i]; - if (block.timestamp >= sub.expiration) { - availableAmount += sub.paidFor - sub.spent; - } - } - return availableAmount; - } - - /** - * @notice Withdraws the contract balance to the beneficiary - * @param amount The amount to withdraw - */ - function withdrawToBeneficiary(uint256 amount) external { - require(msg.sender == beneficiary, "Only the beneficiary can withdraw"); - require( - amount <= calculateAvailableAmountForBeneficiary(), - "Insufficient available amount" - ); - feeToken.safeTransfer(beneficiary, amount); - emit WithdrawalToBeneficiary(beneficiary, amount); - } - - /** - * @notice Cancels a subscription - * @param ritualId The ID of the ritual - * @param subscriptionId The ID of the subscription - */ - function cancelSubscription(uint32 ritualId, uint32 subscriptionId) public virtual { - require( - msg.sender == subscriptions[subscriptionId].subscriber, - "Only the subscriber can cancel the subscription" - ); - uint256 refundAmount = subscriptions[subscriptionId].paidFor; - feeToken.safeTransfer(msg.sender, refundAmount); - delete subscriptions[subscriptionId]; - delete subscribers[LookupKey.lookupKey(ritualId, msg.sender)]; - - emit SubscriptionCancelled(subscriptionId, msg.sender, ritualId); - } -} - -/** - * @title UpfrontSubscriptionWithEncryptorsCap - * @notice Manages upfront subscriptions with a cap on the number of encryptors. - * @dev This contract extends the Subscription contract and introduces a cap on the number of encryptors. - */ -contract UpfrontSubscriptionWithEncryptorsCap is Subscription { - uint256 public constant DEFAULT_CAP = 1000; - - // Mapping from subscription ID to the number of authorization actions - mapping(uint32 => uint256) public authorizationActionCaps; - - /** - * @notice Sets the coordinator and fee token contracts - * @dev The coordinator and fee token contracts cannot be zero addresses - * @param _coordinator The address of the coordinator contract - * @param _feeToken The address of the fee token contract - */ - constructor( - Coordinator _coordinator, - IERC20 _feeToken, - address _beneficiary - ) Subscription(_coordinator, _feeToken, _beneficiary) {} - - /** - * @notice Pays for a subscription and increases the authorization actions cap - * @param subscriptionId The ID of the subscription - */ - function paySubscriptionFor(uint32 subscriptionId) public virtual override { - super.paySubscriptionFor(subscriptionId); - authorizationActionCaps[subscriptionId] += DEFAULT_CAP; - } - - /** - * @notice Returns the authorization actions cap for a given ritual and spender - * @param ritualId The ID of the ritual - * @param spender The address of the spender - * @return The authorization actions cap - */ - function authorizationActionsCap( - uint32 ritualId, - address spender - ) public view returns (uint256) { - return authorizationActionCaps[subscribers[LookupKey.lookupKey(ritualId, spender)]]; - } - - /** - * @notice Cancels a subscription and deletes the authorization actions cap - * @param ritualId The ID of the ritual - * @param subscriptionId The ID of the subscription - */ - function cancelSubscription(uint32 ritualId, uint32 subscriptionId) public virtual override { - super.cancelSubscription(ritualId, subscriptionId); - delete authorizationActionCaps[subscriptionId]; - } -} diff --git a/contracts/test/ManagedAllowListTestSet.sol b/contracts/test/ManagedAllowListTestSet.sol new file mode 100644 index 000000000..54699b45a --- /dev/null +++ b/contracts/test/ManagedAllowListTestSet.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.0; + +import "../contracts/coordination/ITACoRootToChild.sol"; +import "../contracts/coordination/ITACoChildToRoot.sol"; + +contract FeeModelForManagedAllowListMock { + function beforeSetAuthorization( + uint32 ritualId, + address[] calldata addresses, + bool value + ) external { + // solhint-disable-previous-line no-empty-blocks + } + + function beforeIsAuthorized(uint32 ritualId) external view { + // solhint-disable-previous-line no-empty-blocks + } +} + +contract CoordinatorForManagedAllowListMock { + uint256 public numberOfRituals = 1; // for check in GlobalAllowLIst constructor + + mapping(uint32 ritualId => address authority) public authorities; + address public feeModel; + + constructor(address _feeModel) { + feeModel = _feeModel; + } + + function initiateRitual(uint32 ritualId, address authority) external { + authorities[ritualId] = authority; + } + + function isRitualActive(uint32) external pure returns (bool) { + return true; + } + + function getFeeModel(uint32) external view returns (address) { + return feeModel; + } + + function getAuthority(uint32 ritualId) external view returns (address) { + return authorities[ritualId]; + } +} diff --git a/tests/test_managed_allow_list.py b/tests/test_managed_allow_list.py index 05000d854..50590df36 100644 --- a/tests/test_managed_allow_list.py +++ b/tests/test_managed_allow_list.py @@ -1,160 +1,95 @@ +import os + import ape import pytest - -RITUAL_ID = 0 -ADMIN_CAP = 5 -ERC20_SUPPLY = 10**24 -FEE_RATE = 42 +from eth_account.messages import encode_defunct +from web3 import Web3 @pytest.fixture(scope="module") -def deployer(accounts): - return accounts[0] +def initiator(accounts): + initiator_index = 1 + return accounts[initiator_index] @pytest.fixture(scope="module") -def authority(accounts): - return accounts[1] - - -@pytest.fixture(scope="module") -def admin(accounts): - return accounts[2] - - -@pytest.fixture(scope="module") -def encryptor(accounts): - return accounts[3] +def deployer(accounts): + deployer_index = 2 + return accounts[deployer_index] -@pytest.fixture(scope="module") -def beneficiary(accounts): - return accounts[4] +@pytest.fixture() +def fee_model(project, deployer): + contract = project.FeeModelForManagedAllowListMock.deploy(sender=deployer) + return contract @pytest.fixture() -def coordinator(project, deployer): - contract = project.CoordinatorForEncryptionAuthorizerMock.deploy( +def coordinator(project, deployer, fee_model): + contract = project.CoordinatorForManagedAllowListMock.deploy( + fee_model.address, sender=deployer, ) return contract @pytest.fixture() -def fee_token(project, deployer): - return project.TestToken.deploy(ERC20_SUPPLY, sender=deployer) - - -@pytest.fixture() -def subscription(project, coordinator, fee_token, beneficiary, authority): - return project.UpfrontSubscriptionWithEncryptorsCap.deploy( - coordinator.address, fee_token.address, beneficiary, sender=authority +def managed_allow_list(project, deployer, coordinator, oz_dependency): + contract = project.ManagedAllowList.deploy(coordinator.address, sender=deployer) + encoded_initializer_function = b"" + proxy = oz_dependency.TransparentUpgradeableProxy.deploy( + contract.address, + deployer, + encoded_initializer_function, + sender=deployer, ) + proxy_contract = project.ManagedAllowList.at(proxy.address) + return proxy_contract -@pytest.fixture() -def fee_model(project, deployer, coordinator, fee_token): - contract = project.FlatRateFeeModel.deploy( - coordinator.address, fee_token.address, FEE_RATE, sender=deployer - ) - return contract +def test_authorize_using_global_allow_list(coordinator, deployer, initiator, managed_allow_list): + # This block mocks the signature of a threshold decryption request + w3 = Web3() + data = os.urandom(32) + digest = Web3.keccak(data) + signable_message = encode_defunct(digest) + signed_digest = w3.eth.account.sign_message(signable_message, private_key=deployer.private_key) + signature = signed_digest.signature + ritual_id = 0 + cohort_admin_role = managed_allow_list.cohortAdminRole(ritual_id) + auth_admin_role = managed_allow_list.authAdminRole(ritual_id) -@pytest.fixture() -def brand_new_managed_allow_list(project, coordinator, subscription, fee_model, authority): - return project.ManagedAllowList.deploy( - coordinator.address, subscription.address, sender=authority - ) + # Not authorized + assert not managed_allow_list.isAuthorized(0, bytes(signature), bytes(digest)) + # Negative test cases for authorization + with ape.reverts("Only auth admin is permitted"): + managed_allow_list.authorize(ritual_id, [deployer.address], sender=deployer) -@pytest.fixture() -def managed_allow_list(brand_new_managed_allow_list, coordinator, deployer, authority): - coordinator.mockNewRitual(authority, sender=deployer) - return brand_new_managed_allow_list + with ape.reverts("Ritual is not initiated"): + managed_allow_list.initializeCohortAdminRole(ritual_id, sender=deployer) + coordinator.initiateRitual(ritual_id, initiator, sender=initiator) -def test_initial_parameters(managed_allow_list, coordinator, admin, encryptor): - assert managed_allow_list.coordinator() == coordinator.address - assert not managed_allow_list.getAllowance(RITUAL_ID, admin.address) - assert not managed_allow_list.authActions(RITUAL_ID) + managed_allow_list.initializeCohortAdminRole(ritual_id, sender=initiator) + assert managed_allow_list.hasRole(cohort_admin_role, initiator) + managed_allow_list.grantRole(auth_admin_role, deployer, sender=initiator) + assert managed_allow_list.hasRole(auth_admin_role, deployer) + managed_allow_list.authorize(ritual_id, [deployer.address], sender=deployer) -def test_add_administrators(managed_allow_list, authority, admin): - # Only authority can add administrators - with ape.reverts("Only cohort authority is permitted"): - managed_allow_list.addAdministrators(RITUAL_ID, [admin.address], ADMIN_CAP, sender=admin) + managed_allow_list.grantRole(auth_admin_role, initiator, sender=initiator) + with ape.reverts("Encryptor has not been previously authorized by the sender"): + managed_allow_list.deauthorize(ritual_id, [deployer.address], sender=initiator) - tx = managed_allow_list.addAdministrators( - RITUAL_ID, [admin.address], ADMIN_CAP, sender=authority - ) - assert tx.events == [ - managed_allow_list.AdministratorCapSet(RITUAL_ID, admin.address, ADMIN_CAP) - ] - assert managed_allow_list.getAllowance(RITUAL_ID, admin.address) == ADMIN_CAP - assert managed_allow_list.authActions(RITUAL_ID) == 1 - - -def test_remove_administrators(managed_allow_list, authority, admin): - managed_allow_list.addAdministrators(RITUAL_ID, [admin.address], ADMIN_CAP, sender=authority) - assert managed_allow_list.authActions(RITUAL_ID) == 1 - - # Only authority can remove administrators - with ape.reverts("Only cohort authority is permitted"): - managed_allow_list.removeAdministrators(RITUAL_ID, [admin.address], sender=admin) - - tx = managed_allow_list.removeAdministrators(RITUAL_ID, [admin.address], sender=authority) - assert tx.events == [managed_allow_list.AdministratorCapSet(RITUAL_ID, admin.address, 0)] - assert managed_allow_list.getAllowance(RITUAL_ID, admin.address) == 0 - # Auth actions may only increase - assert managed_allow_list.authActions(RITUAL_ID) == 2 - - -@pytest.mark.skip(reason="finish tests when managed allow list will use fee model") -def test_authorize( - managed_allow_list, subscription, fee_token, deployer, authority, admin, encryptor -): - managed_allow_list.addAdministrators(RITUAL_ID, [admin], ADMIN_CAP, sender=authority) - assert managed_allow_list.getAllowance(RITUAL_ID, admin) == ADMIN_CAP - - # Authorization requires a valid and paid for subscription - with ape.reverts("Authorization cap exceeded"): - managed_allow_list.authorize(RITUAL_ID, [encryptor], sender=admin) - - cost = subscription.subscriptionFee() - fee_token.approve(subscription.address, cost, sender=deployer) - tx = subscription.newSubscription(RITUAL_ID, sender=deployer) - assert subscription.numberOfSubscriptions() == 1 - # TODO: Fix this - Currently fails because fee_token is a mock contract - # assert tx.events == [ - # fee_token.Transfer(admin, subscription.address, cost), - # managed_allow_list.AddressAuthorizationSet(RITUAL_ID, admin, True) - # ] - assert len(tx.events) == 3 - assert subscription.authorizationActionsCap(RITUAL_ID, admin) == 1000 - - # Only administrators can authorize encryptors - with ape.reverts("Only administrator is permitted"): - managed_allow_list.authorize(RITUAL_ID, [encryptor], sender=encryptor) - - tx = managed_allow_list.authorize(RITUAL_ID, [encryptor], sender=admin) - assert tx.events == [managed_allow_list.AddressAuthorizationSet(RITUAL_ID, encryptor, True)] - assert managed_allow_list.isAddressAuthorized(RITUAL_ID, encryptor) - - -@pytest.mark.skip(reason="finish tests when managed allow list will use fee model") -def test_deauthorize( - managed_allow_list, subscription, fee_token, deployer, authority, admin, encryptor -): - managed_allow_list.addAdministrators(RITUAL_ID, [admin], ADMIN_CAP, sender=authority) - cost = subscription.subscriptionFee() - fee_token.approve(subscription.address, cost, sender=deployer) - subscription.newSubscription(RITUAL_ID, sender=deployer) - assert subscription.authorizationActionsCap(RITUAL_ID, admin) == 1000 - managed_allow_list.authorize(RITUAL_ID, [encryptor], sender=admin) - - with ape.reverts("Only administrator is permitted"): - managed_allow_list.deauthorize(RITUAL_ID, [encryptor], sender=encryptor) - - tx = managed_allow_list.deauthorize(RITUAL_ID, [encryptor], sender=admin) - assert tx.events == [managed_allow_list.AddressAuthorizationSet(RITUAL_ID, encryptor, False)] - assert not managed_allow_list.isAddressAuthorized(RITUAL_ID, encryptor) + ritual_id = 1 + cohort_admin_role = managed_allow_list.cohortAdminRole(ritual_id) + auth_admin_role = managed_allow_list.authAdminRole(ritual_id) + coordinator.initiateRitual(ritual_id, initiator, sender=initiator) + with ape.reverts(): + managed_allow_list.grantAuthAdminRole(ritual_id, deployer, sender=deployer) + + managed_allow_list.grantAuthAdminRole(ritual_id, deployer, sender=initiator) + assert managed_allow_list.hasRole(cohort_admin_role, initiator) + assert managed_allow_list.hasRole(auth_admin_role, deployer) diff --git a/tests/test_subscription.py b/tests/test_subscription.py deleted file mode 100644 index 3e24065c5..000000000 --- a/tests/test_subscription.py +++ /dev/null @@ -1,165 +0,0 @@ -import ape -import pytest - -RITUAL_ID = 0 -ERC20_SUPPLY = 10 ** 24 - - -@pytest.fixture(scope="module") -def deployer(accounts): - return accounts[0] - - -@pytest.fixture(scope="module") -def authority(accounts): - return accounts[1] - - -@pytest.fixture(scope="module") -def subscriber(accounts): - return accounts[2] - - -@pytest.fixture(scope="module") -def beneficiary(accounts): - return accounts[3] - - -@pytest.fixture(scope="module") -def encryptor(accounts): - return accounts[4] - - -@pytest.fixture() -def coordinator(project, deployer): - contract = project.CoordinatorForEncryptionAuthorizerMock.deploy( - sender=deployer, - ) - return contract - - -@pytest.fixture() -def fee_token(project, deployer): - return project.TestToken.deploy(ERC20_SUPPLY, sender=deployer) - - -@pytest.fixture() -def subscription(project, coordinator, fee_token, beneficiary, authority): - return project.UpfrontSubscriptionWithEncryptorsCap.deploy( - coordinator.address, fee_token.address, beneficiary, sender=authority - ) - - -def assert_new_subscription(subscription, fee_token, deployer, subscriber): - cost = subscription.subscriptionFee() - fee_token.transfer(subscriber, cost, sender=deployer) - - fee_token.approve(subscription.address, cost, sender=subscriber) - tx = subscription.newSubscription(RITUAL_ID, sender=subscriber) - assert subscription.numberOfSubscriptions() == 1 - subscription_id = 0 - # TODO: Fix this - Currently fails because fee_token is a mock contract - # assert tx.events == [ - # fee_token.Transfer(admin, subscription.address, cost), - # ] - assert len(tx.events) == 3 - # assert tx.events[:1] == [ - # subscription.SubscriptionPaid( - # subscription_id, subscriber, cost - # ), - # subscription.SubscriptionCreated( - # subscription_id, subscriber, RITUAL_ID - # ) - # ] - assert subscription.authorizationActionsCap(RITUAL_ID, subscriber) == 1000 - - my_subscription = subscription.subscriptions(subscription_id) - assert my_subscription.subscriber == subscriber - assert my_subscription.paidFor == cost - assert my_subscription.expiration == subscription.baseExpiration() - return subscription_id, my_subscription - - -def test_new_subscription(subscription, fee_token, deployer, subscriber): - assert_new_subscription(subscription, fee_token, deployer, subscriber) - - -def test_cancel_subscription(subscription, fee_token, deployer, subscriber, encryptor): - subscription_id, _ = assert_new_subscription(subscription, fee_token, deployer, subscriber) - - # Only the subscriber can cancel the subscription - with ape.reverts("Only the subscriber can cancel the subscription"): - subscription.cancelSubscription(RITUAL_ID, subscription_id, sender=encryptor) - - tx = subscription.cancelSubscription(RITUAL_ID, subscription_id, sender=subscriber) - assert len(tx.events) == 2 - # TODO: Fix this - Currently fails because fee_token is a mock contract - # assert tx.events == [ - # fee_token.Transfer(subscription.address, subscriber, cost), - # ] - # assert tx.events[:1] == [ - # subscription.SubscriptionCancelled( - # subscription_id, subscriber, RITUAL_ID - # ) - # ] - assert not subscription.subscriptions(subscription_id)[0] - assert not subscription.authorizationActionsCap(RITUAL_ID, subscriber) - - -def test_pay_subscription(subscription, fee_token, deployer, subscriber): - subscription_id, my_subscription = assert_new_subscription(subscription, fee_token, deployer, subscriber) - assert my_subscription.paidFor == subscription.subscriptionFee() - assert my_subscription.expiration == subscription.baseExpiration() - - cost = subscription.subscriptionFee() - fee_token.transfer(subscriber, cost, sender=deployer) - assert fee_token.balanceOf(subscriber) == cost - fee_token.approve(subscription.address, cost, sender=subscriber) - - tx = subscription.paySubscriptionFor(subscription_id, sender=subscriber) - assert len(tx.events) == 2 - # TODO: Fix this - Currently fails because fee_token is a mock contract - # assert tx.events == [ - # fee_token.Transfer(subscription.address, subscriber, cost), - # ] - # assert tx.events[:1] == [ - # subscription.SubscriptionCancelled( - # subscription_id, subscriber, RITUAL_ID - # ) - # ] - # TODO: These are failing, is subscription not being updated? - # assert my_subscription.paidFor == 2 * subscription.subscriptionFee() - # assert my_subscription.expiration == 2 * subscription.baseExpiration() - - -def test_can_spend_from_subscription(subscription, fee_token, deployer, subscriber, coordinator): - subscription_id, _ = assert_new_subscription(subscription, fee_token, deployer, subscriber) - subscription_balance = fee_token.balanceOf(subscription.address) - assert subscription_balance == subscription.subscriptionFee() - - # Only the coordinator can spend from the subscription - with ape.reverts("Unauthorized spender"): - subscription.spendFromSubscription(subscription_id, subscription_balance, sender=subscriber) - - assert not fee_token.balanceOf(coordinator.address) - # TODO: How do I impersonate the coordinator contract here? - # subscription.spendFromSubscription(subscription_id, subscription_balance, sender=coordinator.address) - # assert fee_token.balanceOf(coordinator.address) == subscription_balance - # assert not fee_token.balanceOf(subscription.address) - - -def test_withdraw_to_beneficiary(subscription, fee_token, deployer, subscriber, beneficiary): - subscription_id, _ = assert_new_subscription(subscription, fee_token, deployer, subscriber) - subscription_balance = fee_token.balanceOf(subscription.address) - assert subscription_balance == subscription.subscriptionFee() - - # Only the beneficiary can withdraw from the subscription - with ape.reverts("Only the beneficiary can withdraw"): - subscription.withdrawToBeneficiary(subscription_balance, sender=deployer) - - assert not fee_token.balanceOf(beneficiary) - tx = subscription.withdrawToBeneficiary(subscription_balance, sender=beneficiary) - assert len(tx.events) == 2 - assert tx.events[1] == subscription.WithdrawalToBeneficiary(beneficiary, subscription_balance) - assert fee_token.balanceOf(beneficiary) == subscription_balance - assert not fee_token.balanceOf(subscription.address)