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: add n of m multisignature #864

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
91 changes: 88 additions & 3 deletions kit/contracts/contracts/Bond.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ER
import { ERC20Pausable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { ERC20Capped } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { ERC20MultiSigAccessControl } from "./extensions/ERC20MultiSigAccessControl.sol";
import { ERC20Blocklist } from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Blocklist.sol";
import { ERC20Custodian } from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Custodian.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Expand All @@ -29,7 +29,7 @@ contract Bond is
ERC20Capped,
ERC20Burnable,
ERC20Pausable,
AccessControl,
ERC20MultiSigAccessControl,
ERC20Permit,
ERC20Blocklist,
ERC20Custodian,
Expand Down Expand Up @@ -154,9 +154,11 @@ contract Bond is
uint256 _maturityDate,
uint256 _faceValue,
address _underlyingAsset,
uint256 signatureThreshold,
address forwarder
)
ERC20(name, symbol)
ERC20MultiSigAccessControl(signatureThreshold)
ERC20Permit(name)
ERC20Capped(_cap)
ERC2771Context(forwarder)
Expand Down Expand Up @@ -245,6 +247,25 @@ contract Bond is
/// @param to The address that will receive the minted tokens
/// @param amount The quantity of tokens to create in base units
function mint(address to, uint256 amount) public onlyRole(SUPPLY_MANAGEMENT_ROLE) {
if (signatureThreshold > 1) revert MultiSigRequired();
_mint(to, amount);
}

/// @notice Creates new tokens and assigns them to an address using a multi-signature mechanism
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE. Emits a Transfer event.
/// @param to The address that will receive the minted tokens
/// @param amount The quantity of tokens to create in base units
/// @param signatures An array of EIP-712 signatures from role holders
/// @param operationId A unique identifier for this operation (prevents replay)
function mintWithMultisig(
address to,
uint256 amount,
bytes[] calldata signatures,
bytes32 operationId
)
external
withMultisig(SUPPLY_MANAGEMENT_ROLE, signatures, operationId, keccak256(abi.encode("MINT", to, amount)))
{
_mint(to, amount);
}

Expand Down Expand Up @@ -275,7 +296,26 @@ contract Bond is
/// @notice Closes off the bond at maturity
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE after maturity date
/// @dev Requires sufficient underlying assets for all potential redemptions
function mature() external onlyRole(SUPPLY_MANAGEMENT_ROLE) {
function mature() public onlyRole(SUPPLY_MANAGEMENT_ROLE) {
if (signatureThreshold > 1) revert MultiSigRequired();
_mature();
}

/// @notice Closes off the bond at maturity using a multi-signature mechanism
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE after maturity date
/// @param signatures An array of EIP-712 signatures from role holders
/// @param operationId A unique identifier for this operation (prevents replay)
function matureWithMultisig(
bytes[] calldata signatures,
bytes32 operationId
)
external
withMultisig(SUPPLY_MANAGEMENT_ROLE, signatures, operationId, keccak256(abi.encode("MATURITY")))
{
_mature();
}

function _mature() internal {
if (block.timestamp < maturityDate) revert BondNotYetMatured();
if (isMatured) revert BondAlreadyMatured();

Expand Down Expand Up @@ -303,13 +343,58 @@ contract Bond is
/// @param to The address to send the underlying assets to
/// @param amount The amount of underlying assets to withdraw
function withdrawUnderlyingAsset(address to, uint256 amount) external onlyRole(SUPPLY_MANAGEMENT_ROLE) {
if (signatureThreshold > 1) revert MultiSigRequired();
_withdrawUnderlyingAsset(to, amount);
}

/// @notice Allows withdrawing excess underlying assets using a multi-signature mechanism
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE
/// @param to The address to send the underlying assets to
/// @param amount The amount of underlying assets to withdraw
function withdrawUnderlyingAssetWithMultisig(
address to,
uint256 amount,
bytes[] calldata signatures,
bytes32 operationId
)
external
withMultisig(
SUPPLY_MANAGEMENT_ROLE,
signatures,
operationId,
keccak256(abi.encode("WITHDRAW_UNDERLYING_ASSET", to, amount))
)
{
_withdrawUnderlyingAsset(to, amount);
}

/// @notice Allows withdrawing all excess underlying assets
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE
/// @param to The address to send the underlying assets to
function withdrawExcessUnderlyingAssets(address to) external onlyRole(SUPPLY_MANAGEMENT_ROLE) {
if (signatureThreshold > 1) revert MultiSigRequired();
uint256 withdrawable = withdrawableUnderlyingAmount();
if (withdrawable == 0) revert InsufficientUnderlyingBalance();

_withdrawUnderlyingAsset(to, withdrawable);
}

/// @notice Allows withdrawing all excess underlying assets using a multi-signature mechanism
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE
/// @param to The address to send the underlying assets to
function withdrawExcessUnderlyingAssetsWithMultisig(
address to,
bytes[] calldata signatures,
bytes32 operationId
)
external
withMultisig(
SUPPLY_MANAGEMENT_ROLE,
signatures,
operationId,
keccak256(abi.encode("WITHDRAW_EXCESS_UNDERLYING_ASSETS"))
)
{
uint256 withdrawable = withdrawableUnderlyingAmount();
if (withdrawable == 0) revert InsufficientUnderlyingBalance();

Expand Down
22 changes: 18 additions & 4 deletions kit/contracts/contracts/BondFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,26 @@ contract BondFactory is ReentrancyGuard, ERC2771Context {
uint256 cap,
uint256 maturityDate,
uint256 faceValue,
address underlyingAsset
address underlyingAsset,
uint256 signatureThreshold
)
external
nonReentrant
returns (address bond)
{
bytes32 salt = _calculateSalt(name, symbol, decimals, isin);
address predicted =
predictAddress(_msgSender(), name, symbol, decimals, isin, cap, maturityDate, faceValue, underlyingAsset);
address predicted = predictAddress(
_msgSender(),
name,
symbol,
decimals,
isin,
cap,
maturityDate,
faceValue,
underlyingAsset,
signatureThreshold
);
if (isFactoryToken[predicted]) revert AddressAlreadyDeployed();

Bond newBond = new Bond{ salt: salt }(
Expand All @@ -72,6 +83,7 @@ contract BondFactory is ReentrancyGuard, ERC2771Context {
maturityDate,
faceValue,
underlyingAsset,
signatureThreshold,
trustedForwarder()
);

Expand Down Expand Up @@ -103,7 +115,8 @@ contract BondFactory is ReentrancyGuard, ERC2771Context {
uint256 cap,
uint256 maturityDate,
uint256 faceValue,
address underlyingAsset
address underlyingAsset,
uint256 signatureThreshold
)
public
view
Expand All @@ -123,6 +136,7 @@ contract BondFactory is ReentrancyGuard, ERC2771Context {
maturityDate,
faceValue,
underlyingAsset,
signatureThreshold,
trustedForwarder()
)
)
Expand Down
25 changes: 23 additions & 2 deletions kit/contracts/contracts/Equity.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { ERC20Pausable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { ERC20MultiSigAccessControl } from "./extensions/ERC20MultiSigAccessControl.sol";
import { ERC20Blocklist } from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Blocklist.sol";
import { ERC20Custodian } from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Custodian.sol";
import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
Expand All @@ -26,7 +26,7 @@ contract Equity is
ERC20,
ERC20Burnable,
ERC20Pausable,
AccessControl,
ERC20MultiSigAccessControl,
ERC20Permit,
ERC20Blocklist,
ERC20Custodian,
Expand Down Expand Up @@ -92,9 +92,11 @@ contract Equity is
string memory isin_,
string memory equityClass_,
string memory equityCategory_,
uint256 signatureThreshold,
address forwarder
)
ERC20(name, symbol)
ERC20MultiSigAccessControl(signatureThreshold)
ERC20Permit(name)
ERC2771Context(forwarder)
{
Expand Down Expand Up @@ -177,6 +179,25 @@ contract Equity is
/// @param to The address that will receive the minted tokens
/// @param amount The quantity of tokens to create in base units
function mint(address to, uint256 amount) public onlyRole(SUPPLY_MANAGEMENT_ROLE) {
if (signatureThreshold > 1) revert MultiSigRequired();
_mint(to, amount);
}

/// @notice Creates new tokens and assigns them to an address using a multi-signature mechanism
/// @dev Only callable by addresses with SUPPLY_MANAGEMENT_ROLE. Emits a Transfer event.
/// @param to The address that will receive the minted tokens
/// @param amount The quantity of tokens to create in base units
/// @param signatures An array of EIP-712 signatures from role holders
/// @param operationId A unique identifier for this operation (prevents replay)
function mintWithMultisig(
address to,
uint256 amount,
bytes[] calldata signatures,
bytes32 operationId
)
external
withMultisig(SUPPLY_MANAGEMENT_ROLE, signatures, operationId, keccak256(abi.encode("MINT", to, amount)))
{
_mint(to, amount);
}

Expand Down
20 changes: 16 additions & 4 deletions kit/contracts/contracts/EquityFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,30 @@ contract EquityFactory is ReentrancyGuard, ERC2771Context {
uint8 decimals,
string memory isin,
string memory equityClass,
string memory equityCategory
string memory equityCategory,
uint256 signatureThreshold
)
external
nonReentrant
returns (address token)
{
// Check if address is already deployed
address predicted = predictAddress(_msgSender(), name, symbol, decimals, isin, equityClass, equityCategory);
address predicted =
predictAddress(_msgSender(), name, symbol, decimals, isin, equityClass, equityCategory, signatureThreshold);
if (isFactoryToken[predicted]) revert AddressAlreadyDeployed();

bytes32 salt = _calculateSalt(name, symbol, decimals, isin);

Equity newToken = new Equity{ salt: salt }(
name, symbol, decimals, _msgSender(), isin, equityClass, equityCategory, trustedForwarder()
name,
symbol,
decimals,
_msgSender(),
isin,
equityClass,
equityCategory,
signatureThreshold,
trustedForwarder()
);

token = address(newToken);
Expand All @@ -87,7 +97,8 @@ contract EquityFactory is ReentrancyGuard, ERC2771Context {
uint8 decimals,
string memory isin,
string memory equityClass,
string memory equityCategory
string memory equityCategory,
uint256 signatureThreshold
)
public
view
Expand All @@ -114,6 +125,7 @@ contract EquityFactory is ReentrancyGuard, ERC2771Context {
isin,
equityClass,
equityCategory,
signatureThreshold,
trustedForwarder()
)
)
Expand Down
Loading
Loading