Add allowlist module for gating FCM contracts - #35
Conversation
| /// @title Allowlist | ||
| /// @notice Mapping-backed allowlist of addresses, administered by a single owner. | ||
| /// @dev Idempotent edits: re-adding an existing entry (or removing an absent | ||
| /// one) does not revert and does not emit. Matches OpenZeppelin | ||
| /// `_grantRole` semantics. |
There was a problem hiding this comment.
is there a convention we want to follow for contract/function docs?
There was a problem hiding this comment.
Looks like this is the recommended format: https://docs.soliditylang.org/en/latest/natspec-format.html#natspec. Which I think you're more or less following already here.
| get-allowlist.sh --address 0xABC... --rpc-url https://testnet.evm.nodes.onflow.org \\ | ||
| --blockscout-url "" | ||
| EOF |
There was a problem hiding this comment.
Do you think this script would hit usage limits if the contract was deployed months in the past?
There was a problem hiding this comment.
maybe. I'm going to switch it over to an enumerable set. it roughly double write time gas without impacting reads, but allows us to drop the complex script for minimal extra solidity
There was a problem hiding this comment.
I think a normal mapping would be sufficient here. We should never need a list of all members onchain.
Its unlikely we even need a list offchain, but if we do the typical way of doing it would be to reconstruct it using events.
| /// @title Allowlist | ||
| /// @notice Mapping-backed allowlist of addresses, administered by a single owner. | ||
| /// @dev Idempotent edits: re-adding an existing entry (or removing an absent | ||
| /// one) does not revert and does not emit. Matches OpenZeppelin | ||
| /// `_grantRole` semantics. |
There was a problem hiding this comment.
Looks like this is the recommended format: https://docs.soliditylang.org/en/latest/natspec-format.html#natspec. Which I think you're more or less following already here.
Co-authored-by: Jordan Schalm <jordan.schalm@gmail.com>
Co-authored-by: Jordan Schalm <jordan.schalm@gmail.com>
|
I think it might be simpler to use: pragma solidity ^0.8.20;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract AccessControlERC20Mint is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor(address minter, address burner) ERC20("MyToken", "TKN") {
_grantRole(MINTER_ROLE, minter);
_grantRole(BURNER_ROLE, burner);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
} |
| contract Allowlist is IAllowlistEnumerable, Ownable { | ||
| using EnumerableSet for EnumerableSet.AddressSet; | ||
|
|
||
| EnumerableSet.AddressSet private _allowed; |
There was a problem hiding this comment.
FWIW- Using EnumerableSet (instead of just a dictionary of bools) adds a tiny amount of storage overhead and code complexity while not being strictly necessary. I'm not opposed (especially since the code is already written!), but it would be less code and fewer tests without enumeration support... 🤔
There was a problem hiding this comment.
I originally implemented it this way. The reason I changed was because I think we will ultimately want a way to view the list of allowed addresses. If we use a dictionary, then we need to consume events to rebuild the list. this is much more complex than using EnumerableSet, so moving a small amount of complexity/storage into the contract temporarily seemed like the pragmatic choice.
| /// @dev Idempotent edits: re-adding an existing entry (or removing an absent | ||
| /// one) does not revert and does not emit. Matches OpenZeppelin | ||
| /// `_grantRole` semantics. | ||
| contract Allowlist is IAllowlistEnumerable, Ownable { |
There was a problem hiding this comment.
Strikes me that the most common way for the allow list to be used is via a modifier like onlyAllowed or similar. (Just like onlyOwner from Ownable.) As such, defining that modifier in this contract would be pretty useful!
That's fair. under the covers, they are doing pretty much the same thing. I think the main benefit of using a separate contract is that we could reuse it for multiple vaults or contracts without duplicating the list in multiple places. if we don't think that's going to be needed, AccessControl is simpler. 2 general question @holyfuchs :
|
https://docs.openzeppelin.com/contracts/5.x/access-control#access-management
Probably not for now. If we ever need it switching to the AccessManager should be easy.
I would just deprecate and redeploy. We will most likely redeploy a lot of versions before we ever get to the point of removing the access control. |
| contract FCMVault is ERC4626, AccessControl { | ||
| /// @notice Members of this role may deposit assets, hold shares, and | ||
| /// transfer shares. | ||
| bytes32 public constant ALLOWED_ROLE = keccak256("ALLOWED_ROLE"); |
There was a problem hiding this comment.
| bytes32 public constant ALLOWED_ROLE = keccak256("ALLOWED_ROLE"); | |
| bytes32 public constant EARLY_ACCESS_ROLE = keccak256("EARLY_ACCESS_ROLE"); |
Closes: #9
Summary
IAllowlistinterface (isAllowed(address) → bool+AddressAllowed/AddressDisallowedevents) — intentionally narrow so any backing (mapping, merkle, signature, composite) can be swapped in by consumers.Allowlistcontract:Ownable, mapping-backed, withallow/disallow/allowBatch/disallowBatch. Idempotent edits (no revert and no emit on no-op, matching OZ_grantRolesemantics).ZeroAddresscustom error guards the allow path.FCMVault.solis untouched. Integration into the vault is a follow-up.Integration
The intention is for a new
Allowlistcontract to be deployed for each unique list of address. So the vault could have shared/separate lists for deposits, share holders, etc. The vault can have modifiableIAllowlistimplementation addresses, then set the address toaddress(0)to disable the list.Test plan
make ciis green (fmt-check, build, test)forge testpasses 19/19 (1 existingFCMVaultTest+ 18 newAllowlistTest)forge fmt --checkcleanFCMVault.sol