diff --git a/README.md b/README.md index d71c795..fa52015 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,14 @@ Function to add an allowance for the notary. Invoked only by the contract owner. - `allocatorAddress`: Address of the notary to add allowance for. - `amount`: The amount of allowance to add. +`decreaseAllowance(address allocatorAddress, uint256 amount) external onlyOwner` + +Function to decrease an allowance for the notary. Invoked only by the contract owner. + +- **Parameters:** + - `allocatorAddress`: Address of the notary to add allowance for. + - `amount`: The amount of allowance to decrease. + `setAllowance(address allocatorAddress, uint256 amount) external onlyOwner` Function to set an allowance for the notary. Invoked only by the contract owner. Allowance can be set to 0. To set an allowance bigger than 0, allowance before must equal 0. diff --git a/abis/Allocator.json b/abis/Allocator.json index eb291e5..7537a3e 100644 --- a/abis/Allocator.json +++ b/abis/Allocator.json @@ -79,6 +79,24 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "decreaseAllowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "getAllocators", diff --git a/abis/AllocatorV1.json b/abis/AllocatorV1.json new file mode 100644 index 0000000..eb291e5 --- /dev/null +++ b/abis/AllocatorV1.json @@ -0,0 +1,480 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addAllowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addVerifiedClient", + "inputs": [ + { + "name": "clientAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "allowance_", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllocators", + "inputs": [], + "outputs": [ + { + "name": "allocators", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "initialOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setAllowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "AllowanceChanged", + "inputs": [ + { + "name": "allocator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "allowanceBefore", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "allowanceAfter", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DatacapAllocated", + "inputs": [ + { + "name": "allocator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "client", + "type": "bytes", + "indexed": true, + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ActorError", + "inputs": [ + { + "name": "errorCode", + "type": "int256", + "internalType": "int256" + } + ] + }, + { + "type": "error", + "name": "ActorNotFound", + "inputs": [] + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AlreadyHasAllowance", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyZero", + "inputs": [] + }, + { + "type": "error", + "name": "AmountEqualZero", + "inputs": [] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailToCallActor", + "inputs": [] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "FunctionDisabled", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientAllowance", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidCodec", + "inputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidResponseLength", + "inputs": [] + }, + { + "type": "error", + "name": "NotEnoughBalance", + "inputs": [ + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] diff --git a/abis/IAllocator.json b/abis/IAllocator.json index dd10b36..d834b65 100644 --- a/abis/IAllocator.json +++ b/abis/IAllocator.json @@ -54,6 +54,24 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "decreaseAllowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "getAllocators", diff --git a/abis/IAllocatorV1.json b/abis/IAllocatorV1.json new file mode 100644 index 0000000..dd10b36 --- /dev/null +++ b/abis/IAllocatorV1.json @@ -0,0 +1,163 @@ +[ + { + "type": "function", + "name": "addAllowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addVerifiedClient", + "inputs": [ + { + "name": "clientAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "allowance", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllocators", + "inputs": [], + "outputs": [ + { + "name": "allocators", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setAllowance", + "inputs": [ + { + "name": "allocator", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "AllowanceChanged", + "inputs": [ + { + "name": "allocator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "allowanceBefore", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "allowanceAfter", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DatacapAllocated", + "inputs": [ + { + "name": "allocator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "client", + "type": "bytes", + "indexed": true, + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AlreadyHasAllowance", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyZero", + "inputs": [] + }, + { + "type": "error", + "name": "AmountEqualZero", + "inputs": [] + }, + { + "type": "error", + "name": "FunctionDisabled", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientAllowance", + "inputs": [] + } +] diff --git a/ci/check-full-coverage.sh b/ci/check-full-coverage.sh index a022646..aa5046a 100755 --- a/ci/check-full-coverage.sh +++ b/ci/check-full-coverage.sh @@ -4,10 +4,9 @@ set -euo pipefail cd "$(dirname "$0")"/.. -forge clean && forge coverage --report lcov +forge clean && forge build && forge coverage --no-match-coverage "(script|test|AllocatorV1)" --report lcov -awk '/TN:/,/SF:test\//{if (!/SF:test\// && !/SF:script/) print; if (/TN:/ && /SF:test\//) exit}' lcov.info > lcov_without_tests.info -summary=$(lcov --summary lcov_without_tests.info --rc lcov_branch_coverage=1) +summary=$(lcov --summary lcov.info --rc branch_coverage=1) lines_coverage=$(echo "$summary" | awk '/lines/{print $2}') functions_coverage=$(echo "$summary" | awk '/functions/{print $2}') diff --git a/coverage.sh b/coverage.sh index 0bb428e..c459e46 100755 --- a/coverage.sh +++ b/coverage.sh @@ -3,6 +3,7 @@ set -euo pipefail forge clean +forge build forge coverage --report summary --report lcov genhtml lcov.info -o report --branch-coverage xdg-open report/index.html diff --git a/foundry.toml b/foundry.toml index 3e140bc..6a282f2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ ffi = true build_info = true extra_output = ["storageLayout"] via_ir=false +optimizer=true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options [profile.default.optimizer_details] yul=false diff --git a/src/Allocator.sol b/src/Allocator.sol index 75a081d..efd4134 100644 --- a/src/Allocator.sol +++ b/src/Allocator.sol @@ -96,6 +96,27 @@ contract Allocator is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, I emit AllowanceChanged(allocator, allowanceBefore, allowance(allocator)); } + /** + * @notice Decrease allocator allowance + * @dev This function can only be called by the owner + * @param allocator Allocator whose allowance is reduced + * @param amount Amount to decrease the allowance + * @dev Emits AllowanceChanged event + * @dev Reverts if trying to decrease allowance by 0 + * @dev Reverts if allocator allowance is already 0 + */ + function decreaseAllowance(address allocator, uint256 amount) external onlyOwner { + if (amount == 0) revert AmountEqualZero(); + uint256 allowanceBefore = allowance(allocator); + if (allowanceBefore == 0) { + revert AlreadyZero(); + } else if (allowanceBefore < amount) { + amount = allowanceBefore; + } + _allocators.set(allocator, allowanceBefore - amount); + emit AllowanceChanged(allocator, allowanceBefore, allowance(allocator)); + } + /** * @notice Grant allowance to a client. * @param clientAddress Filecoin address of the client diff --git a/src/AllocatorV1.sol b/src/AllocatorV1.sol new file mode 100644 index 0000000..3e270a6 --- /dev/null +++ b/src/AllocatorV1.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity 0.8.25; + +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IAllocatorV1} from "./interfaces/IAllocatorV1.sol"; +import {VerifRegAPI} from "filecoin-solidity/contracts/v0.8/VerifRegAPI.sol"; +import {VerifRegTypes} from "filecoin-solidity/contracts/v0.8/types/VerifRegTypes.sol"; +import {CommonTypes} from "filecoin-solidity/contracts/v0.8/types/CommonTypes.sol"; +import {FilAddresses} from "filecoin-solidity/contracts/v0.8/utils/FilAddresses.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; + +/** + * @title Allocator + * @notice This contract functions as a middle-man for Allocators. It's made a + * Verifier (a.k.a. Notary) on Filecoin chain and granted DataCap that can then + * be granted to Clients. Granting to clients can be done by Allocators that get + * allowance on the contract, assigned by contract owner. + * @dev Contract is upgradeable via UUPS by contract owner. + */ +contract AllocatorV1 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, IAllocatorV1 { + using EnumerableMap for EnumerableMap.AddressToUintMap; + + /** + * @notice Enumerable mapping from allocator addresses to their current + * allowance + */ + EnumerableMap.AddressToUintMap private _allocators; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Contract initializator. Should be called during deployment. + * @param initialOwner Initial contract owner + */ + function initialize(address initialOwner) public initializer { + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + } + + /** + * @dev Internal. Used by Upgrades logic to check if upgrade is authorized. + * @dev Will revert (reject upgrade) if upgrade isn't called by contract owner. + */ + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + /** + * @notice Get allowance of an allocator + * @param allocator Allocator to get allowance for + * @return allowance_ Allocator's allowance + */ + function allowance(address allocator) public view returns (uint256 allowance_) { + (, allowance_) = _allocators.tryGet(allocator); + } + + /** + * @notice Add allowance to Allocator + * @param allocator Allocator that will receive allowance + * @param amount Amount of allowance to add + * @dev Emits AllowanceChanged event + * @dev Reverts if not called by contract owner + * @dev Reverts if trying to add 0 allowance + */ + function addAllowance(address allocator, uint256 amount) external onlyOwner { + if (amount == 0) revert AmountEqualZero(); + uint256 allowanceBefore = allowance(allocator); + _allocators.set(allocator, allowanceBefore + amount); + emit AllowanceChanged(allocator, allowanceBefore, allowance(allocator)); + } + + /** + * @notice Set allowance of an Allocator. Can be used to remove allowance. + * @param allocator Allocator + * @param amount Amount of allowance to set + * @dev Emits AllowanceChanged event + * @dev Reverts if not called by contract owner + * @dev Reverts if setting to 0 when allocator already has 0 allowance + */ + function setAllowance(address allocator, uint256 amount) external onlyOwner { + uint256 allowanceBefore = allowance(allocator); + if (allowanceBefore == 0 && amount == 0) { + revert AlreadyZero(); + } else if (allowanceBefore > 0 && amount > 0) { + revert AlreadyHasAllowance(); + } else if (allowanceBefore == 0 && amount > 0) { + _allocators.set(allocator, amount); + } else if (allowanceBefore > 0 && amount == 0) { + _allocators.remove(allocator); + } + emit AllowanceChanged(allocator, allowanceBefore, allowance(allocator)); + } + + /** + * @notice Grant allowance to a client. + * @param clientAddress Filecoin address of the client + * @param amount Amount of datacap to grant + * @dev Emits DatacapAllocated event + * @dev Reverts with InsufficientAllowance if caller doesn't have sufficient allowance + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall + */ + function addVerifiedClient(bytes calldata clientAddress, uint256 amount) external { + if (amount == 0) revert AmountEqualZero(); + uint256 allocatorBalance = allowance(msg.sender); + if (allocatorBalance < amount) revert InsufficientAllowance(); + if (allocatorBalance - amount == 0) { + _allocators.remove(msg.sender); + } else { + _allocators.set(msg.sender, allocatorBalance - amount); + } + emit DatacapAllocated(msg.sender, clientAddress, amount); + VerifRegTypes.AddVerifiedClientParams memory params = VerifRegTypes.AddVerifiedClientParams({ + addr: FilAddresses.fromBytes(clientAddress), + allowance: CommonTypes.BigInt(abi.encodePacked(amount), false) + }); + VerifRegAPI.addVerifiedClient(params); + } + + /** + * @notice Get all allocators with non-zero allowance + * @return allocators List of allocators with non-zero allowance + */ + function getAllocators() external view returns (address[] memory allocators) { + return _allocators.keys(); + } + + function renounceOwnership() public view override onlyOwner { + revert FunctionDisabled(); + } +} diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol index 5061826..a285ef3 100644 --- a/src/interfaces/IAllocator.sol +++ b/src/interfaces/IAllocator.sol @@ -70,6 +70,17 @@ interface IAllocator { */ function addAllowance(address allocator, uint256 amount) external; + /** + * @notice Decrease Allocator allowance + * @dev This function can only be called by the owner + * @param allocator Allocator whose allowance is reduced + * @param amount Amount to decrease the allowance + * @dev Emits AllowanceChanged event + * @dev Reverts if trying to decrease allowance by 0 + * @dev Reverts if allocator allowance is already 0 + */ + function decreaseAllowance(address allocator, uint256 amount) external; + /** * @notice Set allowance of an Allocator. Can be used to remove allowance. * @param allocator Allocator diff --git a/src/interfaces/IAllocatorV1.sol b/src/interfaces/IAllocatorV1.sol new file mode 100644 index 0000000..a3ef681 --- /dev/null +++ b/src/interfaces/IAllocatorV1.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +/** + * @title Interface for Allocator contract + * @notice Definition of core functions and events of the Allocator contract + */ +interface IAllocatorV1 { + /** + * @dev Thrown if caller doesn't have enough allowance for given action + */ + error InsufficientAllowance(); + + /** + * @dev Thrown if trying to add 0 allowance or grant 0 datacap + */ + error AmountEqualZero(); + + /** + * @dev Thrown if trying to set allowance bigger than 0 when user has allowance, set allowance to 0 first if you want to set specific value + */ + error AlreadyHasAllowance(); + + /** + * @dev Thrown if trying to set allowance to 0 when it's already 0 + */ + error AlreadyZero(); + + /** + * @dev Thrown if trying to call disabled function + */ + error FunctionDisabled(); + + /** + * @notice Emitted when allocator's allowance is changed by manager + * @param allocator Allocator whose allowance has changed + * @param allowanceBefore Allowance before the change + * @param allowanceAfter Allowance after the change + */ + event AllowanceChanged(address indexed allocator, uint256 allowanceBefore, uint256 allowanceAfter); + + /** + * @notice Emitted when datacap is granted to a client + * @param allocator Allocator who granted the datacap + * @param client Client that received datacap (Filecoin address) + * @param amount Amount of datacap + */ + event DatacapAllocated(address indexed allocator, bytes indexed client, uint256 amount); + + /** + * @notice Get all allocators with non-zero allowance + * @return allocators List of allocators with non-zero allowance + */ + function getAllocators() external view returns (address[] memory allocators); + + /** + * @notice Get allowance of an allocator + * @param allocator Allocator to get allowance for + * @return allowance Allocator's allowance + */ + function allowance(address allocator) external view returns (uint256 allowance); + + /** + * @notice Add allowance to Allocator + * @param allocator Allocator that will receive allowance + * @param amount Amount of allowance to add + * @dev Emits AllowanceChanged event + * @dev Reverts if not called by contract owner + * @dev Reverts if trying to add 0 allowance + */ + function addAllowance(address allocator, uint256 amount) external; + + /** + * @notice Set allowance of an Allocator. Can be used to remove allowance. + * @param allocator Allocator + * @param amount Amount of allowance to set + * @dev Emits AllowanceChanged event + * @dev Reverts if not called by contract owner + * @dev Reverts if setting to 0 when allocator already has 0 allowance + */ + function setAllowance(address allocator, uint256 amount) external; + + /** + * @notice Grant allowance to a client. + * @param clientAddress Filecoin address of the client + * @param amount Amount of datacap to grant + * @dev Emits DatacapAllocated event + * @dev Reverts with InsufficientAllowance if caller doesn't have sufficient allowance + */ + function addVerifiedClient(bytes calldata clientAddress, uint256 amount) external; +} diff --git a/test/Allocator.t.sol b/test/Allocator.t.sol index b500f1f..a56cc87 100644 --- a/test/Allocator.t.sol +++ b/test/Allocator.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.25; import {Test} from "forge-std/Test.sol"; import {Allocator} from "../src/Allocator.sol"; +import {AllocatorV1} from "../src/AllocatorV1.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IAllocator} from "../src/interfaces/IAllocator.sol"; @@ -186,6 +187,22 @@ contract AllocatorTest is Test { allocator.upgradeToAndCall(newImpl, ""); } + function testAllocatorBalanceAfterUpdate() public { + address oldImpl = address(new AllocatorV1()); + ERC1967Proxy proxy = new ERC1967Proxy(oldImpl, abi.encodeCall(Allocator.initialize, (address(this)))); + AllocatorV1 allocatorContract = AllocatorV1(address(proxy)); + allocatorContract.addAllowance(vm.addr(1), 100); + address[] memory allocators = allocatorContract.getAllocators(); + assertEq(allocators[0], vm.addr(1)); + uint256 allowanceBeforeUpdate = allocatorContract.allowance(vm.addr(1)); + address newImpl = address(new Allocator()); + allocatorContract.upgradeToAndCall(newImpl, ""); + uint256 allowanceAfterUpdate = allocatorContract.allowance(vm.addr(1)); + assertEq(allowanceBeforeUpdate, allowanceAfterUpdate); + allocators = allocatorContract.getAllocators(); + assertEq(allocators[0], vm.addr(1)); + } + function testRevertAlreadyZeroAllowance() public { vm.expectRevert(IAllocator.AlreadyZero.selector); allocator.setAllowance(vm.addr(1), 0); @@ -231,4 +248,57 @@ contract AllocatorTest is Test { emit IAllocator.AllowanceChanged(vm.addr(1), allowanceBefore, allowanceBefore + 100); allocator.setAllowance(vm.addr(1), 100); } + + function testDecreaseAllowanceRevertAmountError() public { + vm.expectRevert(abi.encodeWithSelector(IAllocator.AmountEqualZero.selector)); + allocator.decreaseAllowance(vm.addr(1), 0); + } + + function testDecreaseAllowanceRevertNotOwnerError() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, vm.addr(1))); + vm.prank(vm.addr(1)); + allocator.decreaseAllowance(vm.addr(1), 50); + } + + function testDecreaseAllowanceAlreadyZeroError() public { + vm.expectRevert(abi.encodeWithSelector(IAllocator.AlreadyZero.selector)); + allocator.decreaseAllowance(vm.addr(1), 150); + } + + function testDecreaseMoreAllowanceThanClientAlreadyHas() public { + allocator.setAllowance(vm.addr(1), 100); + vm.expectEmit(true, false, false, true); + emit IAllocator.AllowanceChanged(vm.addr(1), 100, 0); + allocator.decreaseAllowance(vm.addr(1), 200); + assertEq(allocator.allowance(vm.addr(1)), 0); + } + + function testDecreaseAllowance() public { + allocator.setAllowance(vm.addr(1), 100); + allocator.decreaseAllowance(vm.addr(1), 50); + uint256 newAllowance = allocator.allowance(vm.addr(1)); + assertEq(100 - 50, newAllowance); + } + + function testDecreaseAllowanceMoreTimes() public { + allocator.setAllowance(vm.addr(1), 100); + allocator.decreaseAllowance(vm.addr(1), 25); + uint256 newAllowance = allocator.allowance(vm.addr(1)); + assertEq(100 - 25, newAllowance); + uint256 allowanceBeforeNextDecrease = allocator.allowance(vm.addr(1)); + allocator.decreaseAllowance(vm.addr(1), 25); + newAllowance = allocator.allowance(vm.addr(1)); + assertEq(allowanceBeforeNextDecrease - 25, newAllowance); + allowanceBeforeNextDecrease = allocator.allowance(vm.addr(1)); + allocator.decreaseAllowance(vm.addr(1), 50); + newAllowance = allocator.allowance(vm.addr(1)); + assertEq(allowanceBeforeNextDecrease - 50, newAllowance); + } + + function testDecreaseAllowanceEmitEvent() public { + allocator.setAllowance(vm.addr(1), 100); + vm.expectEmit(true, false, false, true); + emit IAllocator.AllowanceChanged(vm.addr(1), 100, 50); + allocator.decreaseAllowance(vm.addr(1), 50); + } }