Skip to content

Commit

Permalink
feat: signer and transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
Emmanuel Joseph (JET) committed Oct 1, 2024
1 parent c8714ae commit 93b2f93
Show file tree
Hide file tree
Showing 15 changed files with 741 additions and 46 deletions.
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!---
<!---
Please read this!
Before opening a new pull request (PR), make sure you are merging to the main branch, have your GitHub Issue ID (GID), and understand the popular Git Category Flags (GCF). This will guide the creation of a perfect PR.
Expand Down Expand Up @@ -43,7 +43,7 @@ Please replace this line with your PR overview description to include a summary
<summary>Automation</summary>

### Relevant Issues
<!-- Link relevant GID(s) for which their acceptance criteria are covered by this PR, either as blockers or parent. -->
<!-- Link relevant GID(s) for which their acceptance criteria are covered by this PR, either as blockers or affiliates. -->
-

### Before Merging
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ forge test
forge snapshot
```

## Flatten Contracts for Verification

To flatten the contract for verification (e.g., on [Remix IDE](https://remix.ethereum.org/)), run the flatten command:

```bash
forge flatten ./src/MultiSigEnterpriseVault.sol > ./.private/MultiSigEnterpriseVault.sol
```

This command outputs a single Solidity file containing all dependencies.

## License

```md
Expand Down
12 changes: 4 additions & 8 deletions src/MultiSigEnterpriseVault.sol
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;

import {User} from './components/User.sol';
import {AddressUtils} from './libraries/AddressUtils.sol';
import {IMultiSigEnterpriseVault} from './interfaces/IMultiSigEnterpriseVault.sol';
import {MultiSigTransaction} from './components/MultiSigTransaction.sol';
import {AddressUtils} from './libraries/AddressUtils.sol';

/**
* @title MultiSig Enterprise Vault Contract
* @author Emmanuel Joseph (JET)
*/
contract MultiSigEnterpriseVault is User, IMultiSigEnterpriseVault {
/// @notice The current threshold required for signatory approval.
uint256 public signatoryThreshold;

contract MultiSigEnterpriseVault is MultiSigTransaction, IMultiSigEnterpriseVault {
/**
* @dev Initializes the MultiSigEnterpriseVault with the owner, initial signatory threshold, and owner override limit.
* @param owner The address of the contract owner.
Expand All @@ -23,9 +20,8 @@ contract MultiSigEnterpriseVault is User, IMultiSigEnterpriseVault {
address owner,
uint256 initialThreshold,
uint256 initialOwnerOverrideLimit
) User(owner, initialOwnerOverrideLimit) {
) MultiSigTransaction(owner, initialThreshold, initialOwnerOverrideLimit) {
AddressUtils.requireValidUserAddress(owner);
signatoryThreshold = initialThreshold;
}

/**
Expand Down
234 changes: 234 additions & 0 deletions src/components/MultiSigTransaction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;

import '../libraries/Counters.sol';
import '../libraries/AddressUtils.sol';
import '../utilities/VaultConstants.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {IMultiSigTransaction} from '../interfaces/IMultiSigTransaction.sol';
import {Transaction} from '../utilities/VaultStructs.sol';
import {ArraysUtils} from '../libraries/ArraysUtils.sol';
import {User} from './User.sol';

/**
* @title MultiSigTransaction
* @dev Manages transaction initiation, approval, and execution for ETH and ERC20 transfers.
*/
abstract contract MultiSigTransaction is User, IMultiSigTransaction {
using Counters for Counters.Counter;

/// @notice The minimum number of approvals to execute a transaction
uint256 public signatoryThreshold;

/// @notice Using Counter library for total transactions
Counters.Counter private _transactionCount;

/// @notice Mapping to store all transactions by their ID
mapping(uint256 => Transaction) private _transactions;

/// @notice Mapping to track approvals for each transaction by signers
mapping(uint256 => mapping(address => bool)) public approvals;

/**
* @dev Initializes the `MultiSigTransaction` and `User` contracts.
* @param owner The address of the contract owner.
* @param initialThreshold The initial threshold for signatory approval.
* @param initialOwnerOverrideLimit The initial timelock limit for owner override.
*/
constructor(
address owner,
uint256 initialThreshold,
uint256 initialOwnerOverrideLimit
) User(owner, initialOwnerOverrideLimit) {
signatoryThreshold = initialThreshold;
}

/**
* @dev Modifier to ensure a valid vault transaction.
* Reverts with `InvalidTransaction` if the given transaction ID is invalid.
* @param transactionId The ID of the transaction to validate.
*/
modifier validTransaction(
uint256 transactionId
) {
if (transactionId == 0 || transactionId < _transactionCount.current()) {
revert InvalidTransaction(transactionId);
}
_;
}

/**
* Receives ETH sent to the contract and emits `FundsReceived` event
*/
receive() external payable {
emit FundsReceived(_msgSender(), address(0), msg.value);
}

/**
* @inheritdoc IMultiSigTransaction
*/
function depositToken(address token, uint256 amount) external payable {
AddressUtils.requireValidTokenAddress(token);
require(amount >= msg.value, 'MultiSigTransaction: Invalid deposit amount');

IERC20(token).transferFrom(_msgSender(), address(this), msg.value);
emit FundsReceived(_msgSender(), token, msg.value);
}

/**
* @notice Returns the balance of ETH held in the contract.
* @return The ETH balance of the contract.
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}

/**
* @notice Returns the balance of a specific ERC20 token held in the contract.
* @param token The ERC20 token address.
* @return The token balance of the contract.
*/
function getTokenBalance(address token) public view returns (uint256) {
AddressUtils.requireValidTokenAddress(token);
return IERC20(token).balanceOf(address(this));
}

/**
* @notice Returns the total number of transactions.
* @return uint256 The total number of transactions.
* @dev Only callable by the owner.
*/
function totalTransactions() public view returns (uint256) {
return _transactionCount.current();
}

/**
* @notice Returns the transaction details of a given ID.
* @param transactionId The ID of the requested transaction.
* @return Transaction The transaction object associated with the provided ID.
*/
function getTransaction(
uint256 transactionId
) public view validTransaction(transactionId) onlyUser returns (Transaction memory) {
return _transactions[transactionId];
}

/**
* @notice Returns the total number of approvals a transaction has.
* @param transactionId The ID of the requested transaction.
* @return uint256 The transaction total number of approvers.
*/
function getTransactionApprovals(
uint256 transactionId
) public view validTransaction(transactionId) onlyUser returns (uint256) {
return _transactions[transactionId].approvals.current();
}

/**
* @notice Returns the signatures associated with a transaction.
* @param transactionId The ID of the requested transaction.
* @return address[] The transaction signatures array.
*/
function getTransactionSignatures(
uint256 transactionId
) public view validTransaction(transactionId) onlyUser returns (address[] memory) {
return _transactions[transactionId].signatures;
}

/**
* @notice Initiates a new transaction by an Owner or Signer.
* @param target The target address for the transaction.
* @param value The amount of ETH or tokens to send.
* @param token The ERC20 token address (0x0 for ETH).
* @param data The transaction data (0x0 for empty data).
*/
function initiateTransaction(
address payable target,
address token,
uint256 value,
bytes memory data
) public validSigner {
AddressUtils.requireValidTransactionTarget(target);
if (token == address(0)) {
if (value > getBalance()) revert InsufficientTokenBalance(getBalance(), value);
} else {
if (value > getTokenBalance(token)) revert InsufficientTokenBalance(getTokenBalance(token), value);
}

_transactionCount.increment();
uint256 transactionId = _transactionCount.current();
Transaction storage txn = _transactions[transactionId];

txn.initiator = _msgSender();
txn.target = target;
txn.token = token;
txn.value = value;
txn.data = data;

emit TransactionInitiated(transactionId, _msgSender(), target, token, value);
}

/**
* @notice Enables valid signers to approve a transaction.
* @param transactionId The ID of the transaction to approve.
*/
function approveTransaction(
uint256 transactionId
) public validTransaction(transactionId) validSigner {
Transaction storage txn = _transactions[transactionId];
if (txn.isExecuted) revert TransactionAlreadyExecuted(transactionId);
if (approvals[transactionId][_msgSender()]) revert TransactionNotApproved(transactionId);

txn.approvals.increment();
txn.signatures.push(_msgSender());
approvals[transactionId][_msgSender()] = true;
emit TransactionApproved(transactionId, _msgSender(), block.timestamp);
}

/**
* @notice Enables valid signers to revokes a transaction approval.
* @param transactionId The ID of the transaction to revoke approval for.
*/
function revokeApproval(
uint256 transactionId
) public validTransaction(transactionId) validSigner {
Transaction storage txn = _transactions[transactionId];
if (txn.isExecuted) revert TransactionAlreadyExecuted(transactionId);
if (!approvals[transactionId][_msgSender()]) revert TransactionNotApproved(transactionId);

txn.approvals.decrement();
approvals[transactionId][_msgSender()] = false;
uint256 signerSignatureIndex = ArraysUtils.arrayElementIndexLookup(_msgSender(), txn.signatures);
ArraysUtils.removeElementFromArray(signerSignatureIndex, txn.signatures);

emit TransactionRevoked(transactionId, _msgSender(), block.timestamp);
}

/**
* @notice Executes a transaction if the threshold is met.
* @param transactionId The ID of the transaction to execute.
*/
function executeTransaction(
uint256 transactionId
) public validTransaction(transactionId) validExecutor {
Transaction storage txn = _transactions[transactionId];
if (txn.isExecuted) revert TransactionAlreadyExecuted(transactionId);
if (txn.approvals.current() < signatoryThreshold) revert InsufficientApprovals(signatoryThreshold, txn.approvals.current());
if (hasRole(OWNER_ROLE, _msgSender())) {
if (!approvals[transactionId][_msgSender()]) revert TransactionNotApproved(transactionId);
}

txn.isExecuted = true;
if (txn.token == address(0)) {
// Send ETH
Address.sendValue(txn.target, txn.value);
} else {
// Send ERC20 tokens
IERC20 token = IERC20(txn.token);
SafeERC20.safeTransfer(token, txn.target, txn.value);
}

emit TransactionExecuted(transactionId, _msgSender(), block.timestamp);
}
}
49 changes: 40 additions & 9 deletions src/components/User.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.27;

import '../libraries/Counters.sol';
import '../utilities/VaultConstants.sol';
import {IUser} from '../interfaces/IUser.sol';
import {OwnerRole} from './roles/OwnerRole.sol';
import {SignerRole} from './roles/SignerRole.sol';
Expand Down Expand Up @@ -39,7 +40,7 @@ abstract contract User is OwnerRole, ExecutorRole, SignerRole, IUser {

/**
* @dev Modifier to restrict access to functions to only vault users.
* Reverts with `AccessControlUnauthorizedSigner` if the caller is not the signer.
* Reverts with `InvalidUserProfile` if the caller is not a user.
*/
modifier onlyUser() {
if (!_isUser(_msgSender())) {
Expand All @@ -48,6 +49,42 @@ abstract contract User is OwnerRole, ExecutorRole, SignerRole, IUser {
_;
}

/**
* @dev Modifier to ensure a valid vault user.
* Reverts with `InvalidUserProfile` if the given user address is invalid.
* @param user The user address to validate.
*/
modifier validUser(
address user
) {
if (!_isUser(user)) {
revert InvalidUserProfile(user);
}
_;
}

/**
* @dev Modifier to restrict access to functions to valid signers.
* Reverts with `UnauthorizedTransactionSigner` if the caller is not an authorized transaction signer.
*/
modifier validSigner() {
if (!isSigner(_msgSender()) && !hasRole(OWNER_ROLE, _msgSender())) {
revert UnauthorizedTransactionSigner(_msgSender());
}
_;
}

/**
* @dev Modifier to restrict access to functions to valid executors.
* Reverts with `UnauthorizedTransactionExecutor` if the caller is not an authorized transaction executor.
*/
modifier validExecutor() {
if (!hasRole(EXECUTOR_ROLE, _msgSender()) && !hasRole(OWNER_ROLE, _msgSender())) {
revert UnauthorizedTransactionExecutor(_msgSender());
}
_;
}

/**
* @notice Returns the total number of users.
* @return uint256 The total number of users.
Expand All @@ -68,10 +105,7 @@ abstract contract User is OwnerRole, ExecutorRole, SignerRole, IUser {
*/
function getUserProfile(
address user
) public view onlyOwner returns (UserProfile memory) {
if (!_isUser(user)) {
revert InvalidUserProfile(user);
}
) public view validUser(user) onlyOwner returns (UserProfile memory) {
return _users[user];
}

Expand Down Expand Up @@ -190,10 +224,7 @@ abstract contract User is OwnerRole, ExecutorRole, SignerRole, IUser {
*/
function _removeUser(
address user
) private {
UserProfile storage profile = _users[user];
profile.user.requireValidUserAddress();

) private validUser(user) {
delete _users[user];
_userCount.decrement();
}
Expand Down
Loading

0 comments on commit 93b2f93

Please sign in to comment.