From f37c984b33396fa3476f8c02950707bfb243a65c Mon Sep 17 00:00:00 2001 From: Stanislav Chernenko <64794434+stanis-chernes@users.noreply.github.com> Date: Wed, 14 May 2025 20:14:01 +0200 Subject: [PATCH 1/3] feat: VaultMultisig, AccessManager --- .github/workflows/test.yml | 43 +++++++ .gitignore | 13 +- .gitmodules | 6 + README.md | 73 +++++++++--- foundry.toml | 7 ++ lib/forge-std | 1 + lib/openzeppelin-contracts | 1 + src/AccessManager.sol | 24 ++++ src/Roles.sol | 6 + src/VaultMultisig.sol | 237 +++++++++++++++++++++++++++++++++++++ 10 files changed, 395 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 .gitmodules create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 160000 lib/openzeppelin-contracts create mode 100644 src/AccessManager.sol create mode 100644 src/Roles.sol create mode 100644 src/VaultMultisig.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..34a4a52 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/.gitignore b/.gitignore index a49555a..9c549b5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,15 @@ states/ # Optional .env -.env.local \ No newline at end of file +.env.local + +# Foundry +.env.foundry +out/ +cache/ + +# VSCode +.vscode/ + +# Node +node_modules/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..690924b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index 251accb..9265b45 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,66 @@ -# Smart Modules +## Foundry -**Smart Modules** is a curated collection of Solidity components built for developers who want to master real-world smart contract engineering. +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** -This repository is part of an [Solidity Bootcamp](https://bootcamp.solidity.university) program by [Solidity University](https://solidity.university), covering core topics like: +Foundry consists of: -- ✅ Multisig wallets -- ✅ Gasless transactions -- ✅ EIP-712 signatures -- ✅ TBA +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +## Documentation -> 🚧 **This repository is under active development.** Expect new modules, upgrades, and educational content in upcoming commits. +https://book.getfoundry.sh/ ---- +## Usage -## 🚀 Getting Started +### Build -```bash -git clone https://github.com/solidity-university/smart-modules.git -cd smart-modules -forge install -forge build +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..2d9cb54 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc_version = "0.8.30" + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..77041d2 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..e4f7021 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/src/AccessManager.sol b/src/AccessManager.sol new file mode 100644 index 0000000..1e2aed7 --- /dev/null +++ b/src/AccessManager.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.8.30; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./Roles.sol"; +contract AccessManager is AccessControl { + using Roles for bytes32; + + constructor() { + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setRoleAdmin(Roles.MULTISIG_ADMIN_ROLE, DEFAULT_ADMIN_ROLE); + } + + function addMultisigAdmin(address _multisigAdmin) external onlyRole(DEFAULT_ADMIN_ROLE) { + _grantRole(Roles.MULTISIG_ADMIN_ROLE, _multisigAdmin); + } + + function removeMultisigAdmin(address _multisigAdmin) external onlyRole(DEFAULT_ADMIN_ROLE) { + _revokeRole(Roles.MULTISIG_ADMIN_ROLE, _multisigAdmin); + } + + function isMultisigAdmin(address _address) external view returns (bool) { + return hasRole(Roles.MULTISIG_ADMIN_ROLE, _address); + } +} \ No newline at end of file diff --git a/src/Roles.sol b/src/Roles.sol new file mode 100644 index 0000000..1b0c7af --- /dev/null +++ b/src/Roles.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.8.30; + +library Roles { + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + bytes32 public constant MULTISIG_ADMIN_ROLE = keccak256("MULTISIG_ADMIN_ROLE"); +} \ No newline at end of file diff --git a/src/VaultMultisig.sol b/src/VaultMultisig.sol new file mode 100644 index 0000000..6dca6f0 --- /dev/null +++ b/src/VaultMultisig.sol @@ -0,0 +1,237 @@ +pragma solidity ^0.8.30; + +import "./AccessManager.sol"; + +contract VaultMultisig { + /// @notice The number of signatures required to execute a transaction + uint256 public quorum; + + /// @notice The number of transfers executed + uint256 public transfersCount; + + /// @notice The access manager + AccessManager public accessManager; + + /// @notice The current multisig signers + address[] public currentMultiSigSigners; + + /// @dev The struct is used to store the details of a transfer + /// @param to The address of the recipient + /// @param amount The amount of tokens to transfer + /// @param approvals The number of approvals required to execute the transfer + /// @param executed Whether the transfer has been executed + /// @param approved The mapping of signers to their approval status + struct Transfer { + address to; + uint256 amount; + uint256 approvals; + bool executed; + mapping(address => bool) approved; + } + + /// @notice The mapping of transfer IDs to transfer details + mapping (uint256 => Transfer) private transfers; + + /// @notice The mapping for verification that address is a signer + mapping (address => bool) private multiSigSigners; + + /// @notice Checks that signers array is not empty + error SignersArrayCannotBeEmpty(); + + /// @notice Checks that quorum is not greather than the number of signers + error QuorumGreaterThanSigners(); + + /// @notice Checks that quorum is greater than zero + error QuorumCannotBeZero(); + + /// @notice Checks that the recipient is not the zero address + error InvalidRecipient(); + + /// @notice Checks that amount is greater than zero + error InvalidAmount(); + + /// @notice Checks that the signer is a multisig signer + error InvalidMultisigSigner(); + + /// @notice Checks that the balance is sufficient for the transfer + error InsufficientBalance(uint256 balance, uint256 desiredAmount); + + /// @notice Checks that the transfer is not already executed + /// @param transferId The ID of the transfer + error TransferIsAlreadyExecuted(uint256 transferId); + + /// @notice Checks that the signer is already approved + /// @param signer The address of the signer + error SignerAlreadyApproved(address signer); + + /// @notice Checks that the transfer failed + /// @param transferId The ID of the transfer + error TransferFailed(uint256 transferId); + + /// @notice Checks that quorum was reached for transfer + /// @param transferId The ID of the transfer + error QuorumHasNotBeenReached(uint256 transferId); + + /// @notice Checks that the signer is a multisig admin + error InvalidMultisigAdmin(); + + /// @notice Emitted when a transfer is initiated + event TransferInitiated(uint256 indexed transferId, address indexed to, uint256 amount); + + /// @notice Emitted when a transfer is approved + /// @param transferId The ID of the transfer + /// @param approver The address of the approver + event TransferApproved(uint256 indexed transferId, address indexed approver); + + /// @notice Emitted when a transfer is executed + /// @param transferId The ID of the transfer + event TransferExecuted(uint256 indexed transferId); + + /// @notice Emitted when the multisig signers are updated + event MultiSigSignersUpdated(); + + /// @notice Emitted when the quorum is updated + /// @param quorum The new quorum + event QuorumUpdated(uint256 quorum); + + modifier onlyMultisigSigner() { + if (!multiSigSigners[msg.sender]) revert InvalidMultisigSigner(); + _; + } + + modifier onlyMultisigAdmin() { + if (!accessManager.isMultisigAdmin(msg.sender)) revert InvalidMultisigAdmin(); + _; + } + + /// @notice Initializes the multisig contract + /// @param _signers The array of multisig signers + /// @param _quorum The number of signatures required to execute a transaction + constructor( + address[] memory _signers, + uint256 _quorum, + address _accessManager + ) { + if (_signers.length == 0) revert SignersArrayCannotBeEmpty(); + if (_quorum > _signers.length) revert QuorumGreaterThanSigners(); + if (_quorum == 0) revert QuorumCannotBeZero(); + + for (uint256 i = 0; i < _signers.length; i++) { + multiSigSigners[_signers[i]] = true; + } + + quorum = _quorum; + accessManager = AccessManager(_accessManager); + } + + /// @notice Updates the multisig signers + /// @param _signers The array of multisig signers + function updateSigners(address[] memory _signers) external onlyMultisigAdmin { + if (_signers.length == 0) revert SignersArrayCannotBeEmpty(); + if (_signers.length < quorum) revert QuorumGreaterThanSigners(); + + for (uint256 i = 0; i < currentMultiSigSigners.length; i++) { + multiSigSigners[currentMultiSigSigners[i]] = false; + } + + for (uint256 i = 0; i < _signers.length; i++) { + multiSigSigners[_signers[i]] = true; + } + + currentMultiSigSigners = _signers; + + emit MultiSigSignersUpdated(); + } + + /// @notice Updates the quorum + /// @param _quorum The new quorum + function updateQuorum(uint256 _quorum) external onlyMultisigAdmin { + if (_quorum > currentMultiSigSigners.length) revert QuorumGreaterThanSigners(); + if (_quorum == 0) revert QuorumCannotBeZero(); + + quorum = _quorum; + + emit QuorumUpdated(_quorum); + } + + /// @notice Initiates a transfer + /// @param _to The address of the recipient + /// @param _amount The amount of tokens to transfer + function initiateTransfer(address _to, uint256 _amount) external onlyMultisigSigner { + if (_to == address(0)) revert InvalidRecipient(); + if (_amount <= 0) revert InvalidAmount(); + + uint256 transferId = transfersCount++; + Transfer storage transfer = transfers[transferId]; + transfer.to = _to; + transfer.amount = _amount; + transfer.approvals = 0; + transfer.executed = false; + transfer.approved[msg.sender] = true; + + emit TransferInitiated(transferId, _to, _amount); + } + + /// @notice Approves a transfer + /// @param _transferId The ID of the transfer + function approveTransfer(uint256 _transferId) external onlyMultisigSigner { + Transfer storage transfer = transfers[_transferId]; + if (transfer.executed) revert TransferIsAlreadyExecuted(_transferId); + if (transfer.approved[msg.sender]) revert SignerAlreadyApproved(msg.sender); + + transfer.approvals++; + transfer.approved[msg.sender] = true; + + emit TransferApproved(_transferId, msg.sender); + } + + function executeTransfer(uint256 _transferId) external onlyMultisigSigner { + Transfer storage transfer = transfers[_transferId]; + if (transfer.approvals < quorum) revert QuorumHasNotBeenReached(_transferId); + if (transfer.executed) revert TransferIsAlreadyExecuted(_transferId); + + uint256 balance = address(this).balance; + if (transfer.amount >= balance) revert InsufficientBalance(balance, transfer.amount); + + (bool success, ) = transfer.to.call{value: transfer.amount}(""); + if (!success) revert TransferFailed(_transferId); + + transfer.executed = true; + + emit TransferExecuted(_transferId); + } + + /// @notice Default fallback function for receiving ETH + receive() external payable {} + + /// @notice Gets the details of a transfer + /// @param _transferId The ID of the transfer + /// @return to The address of the recipient + /// @return amount The amount of tokens to transfer + /// @return approvals The number of approvals required to execute the transfer + /// @return executed Whether the transfer has been executed + function getTransfer(uint256 _transferId) external view returns ( + address to, + uint256 amount, + uint256 approvals, + bool executed + ) { + Transfer storage transfer = transfers[_transferId]; + return (transfer.to, transfer.amount, transfer.approvals, transfer.executed); + } + + /// @notice Checks if a signer has signed a transfer + /// @param _transferId The ID of the transfer + /// @param _signer The address of the signer + /// @return hasSigned Whether the signer has signed the transfer + function hasSignedTransfer(uint256 _transferId, address _signer) external view returns (bool) { + Transfer storage transfer = transfers[_transferId]; + return transfer.approved[_signer]; + } + + /// @notice Gets the number of transfers + /// @return The number of transfers + function getTransferCount() external view returns (uint256) { + return transfersCount; + } +} \ No newline at end of file From 69a74dda92eb9e04020c8243b7aeba521a14994a Mon Sep 17 00:00:00 2001 From: Stanislav Chernenko <64794434+stanis-chernes@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:16:26 +0200 Subject: [PATCH 2/3] feat: lesson21 --- .gitmodules | 3 + lib/openzeppelin-contracts-upgradeable | 1 + src/EIP712Swap.sol | 46 +++++++++ src/FeeManager.sol | 29 ++++++ src/ISwap.sol | 37 +++++++ src/LiquidityPool.sol | 137 +++++++++++++++++++++++++ 6 files changed, 253 insertions(+) create mode 160000 lib/openzeppelin-contracts-upgradeable create mode 100644 src/EIP712Swap.sol create mode 100644 src/FeeManager.sol create mode 100644 src/ISwap.sol create mode 100644 src/LiquidityPool.sol diff --git a/.gitmodules b/.gitmodules index 690924b..9296efd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..60b305a --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 60b305a8f3ff0c7688f02ac470417b6bbf1c4d27 diff --git a/src/EIP712Swap.sol b/src/EIP712Swap.sol new file mode 100644 index 0000000..9161f57 --- /dev/null +++ b/src/EIP712Swap.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.8.30; + +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "./LiquidityPool.sol"; +import "./ISwap.sol"; + +contract EIP712Swap is EIP712 { + using ECDSA for bytes32; + + mapping(address => uint256) private _nonces; + + bytes32 private constant SWAP_TYPEHASH = + keccak256("SwapRequest(address pool,address sender,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 nonce,uint256 deadline)"); + + error InvalidSignature(); + error ExpiredSwapRequest(); + error InvalidNonce(); + + constructor() EIP712("EIP712Swap", "1") {} + + function getDomainSeparator() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + function getNonce(address _sender) public view returns (uint256) { + return _nonces[_sender]; + } + + function verify(ISwap.SwapRequest memory _swapRequest, bytes memory _signature) public view returns (bool) { + bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(SWAP_TYPEHASH, _swapRequest.pool, _swapRequest.sender, _swapRequest.tokenIn, _swapRequest.tokenOut, _swapRequest.amountIn, _swapRequest.minAmountOut, _swapRequest.nonce, _swapRequest.deadline))); + address signer = digest.recover(_signature); + return signer == _swapRequest.sender; + } + + function executeSwap(ISwap.SwapRequest memory _swapRequest, bytes memory _signature) public returns (bool) { + if (!verify(_swapRequest, _signature)) revert InvalidSignature(); + if (_swapRequest.deadline < block.timestamp) revert ExpiredSwapRequest(); + if (_swapRequest.nonce != _nonces[_swapRequest.sender]) revert InvalidNonce(); + + _nonces[_swapRequest.sender]++; + LiquidityPool(_swapRequest.pool).swap(_swapRequest.sender, _swapRequest.tokenIn, _swapRequest.tokenOut, _swapRequest.amountIn, _swapRequest.minAmountOut); + + return true; + } +} \ No newline at end of file diff --git a/src/FeeManager.sol b/src/FeeManager.sol new file mode 100644 index 0000000..fd64206 --- /dev/null +++ b/src/FeeManager.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.8.30; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import "./ISwap.sol"; + +contract FeeManager is Initializable, AccessControlUpgradeable, UUPSUpgradeable { + uint256 public constant FEE_DENOMINATOR = 10000; + + uint256 public fee; + + function initialize(uint256 _fee) external initializer { + _disableInitializers(); + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + fee = _fee; + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} + + function setFee(uint256 _fee) external onlyRole(DEFAULT_ADMIN_ROLE) { + fee = _fee; + } + + function getFee(ISwap.SwapParams memory swapParams) external view returns (uint256) { + return (swapParams.amount0 * fee) / FEE_DENOMINATOR; + } +} \ No newline at end of file diff --git a/src/ISwap.sol b/src/ISwap.sol new file mode 100644 index 0000000..6808882 --- /dev/null +++ b/src/ISwap.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.8.30; + +interface ISwap { + /// @notice Parameters for the swap + /// @param token0 The address of the first token + /// @param token1 The address of the second token + /// @param amount0 The amount of the first token to swap + /// @param reserveToken0 The reserve of the first token + /// @param reserveToken1 The reserve of the second token + struct SwapParams { + address token0; + address token1; + uint256 amount0; + uint256 reserveToken0; + uint256 reserveToken1; + } + + /// @notice Parameters for the swap request + /// @param pool The address of the pool + /// @param sender The address of the sender + /// @param tokenIn The address of the token to swap in + /// @param tokenOut The address of the token to swap out + /// @param amountIn The amount of the token to swap in + /// @param minAmountOut The minimum amount of the token to swap out + /// @param nonce The nonce of the swap + /// @param deadline The deadline of the swap + struct SwapRequest { + address pool; + address sender; + address tokenIn; + address tokenOut; + uint256 amountIn; + uint256 minAmountOut; + uint256 nonce; + uint256 deadline; + } +} \ No newline at end of file diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol new file mode 100644 index 0000000..8a629c5 --- /dev/null +++ b/src/LiquidityPool.sol @@ -0,0 +1,137 @@ +pragma solidity ^0.8.30; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./ISwap.sol"; +import "./FeeManager.sol"; +import "./EIP712Swap.sol"; + +contract LiquidityPool is ISwap, FeeManager { + address public token0; + uint256 public token0Decimals; + address public token1; + uint256 public token1Decimals; + uint256 public reserveToken0; + uint256 public reserveToken1; + FeeManager public feeManager; + EIP712Swap public eip712Swap; + + // @notice Emitted when liquidity is added to the pool + /// @param _token The token that was added + /// @param _amount The amount of tokens that were added + event LiquidityAdded(address indexed _token, uint256 _amount); + + // @notice Emitted when a swap is executed + /// @param _tokenIn The token that was swapped in + /// @param _tokenOut The token that was swapped out + /// @param _amountIn The amount of tokens that were swapped in + /// @param _amountOut The amount of tokens that were swapped out + event Swap(address indexed _tokenIn, address indexed _tokenOut, uint256 _amountIn, uint256 _amountOut); + + error InsufficientTokenBalance(); + error InvalidTokenAddress(address _token); + error InvalidTokenPair(address _tokenIn, address _tokenOut); + error InsufficientLiquidity(); + error InsufficientOutputAmount(uint256 expected, uint256 actual); + error ExcessiveInputAmount(uint256 expected, uint256 actual); + error InsufficientAllowance(); + + constructor(address _token0, uint256 _token0Decimals, address _token1, uint256 _token1Decimals, address _feeManager, address _eip712Swap) { + token0 = _token0; + token0Decimals = _token0Decimals; + token1 = _token1; + token1Decimals = _token1Decimals; + feeManager = FeeManager(_feeManager); + eip712Swap = EIP712Swap(_eip712Swap); + } + + // @notice Add liquidity to the pool + /// @param _token The token to add liquidity for + /// @param _amount The amount of tokens to add + function addLiquidity(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_token == token0 || _token == token1) { + revert InvalidTokenAddress(_token); + } + + if (IERC20(_token).balanceOf(address(msg.sender)) < _amount) { + revert InsufficientTokenBalance(); + } + + require(IERC20(_token).transferFrom(msg.sender, address(this), _amount)); + + if (_token == token0) { + reserveToken0 += _amount; + } else if (_token == token1) { + reserveToken1 += _amount; + } + + emit LiquidityAdded(_token, _amount); + } + + // @notice Get the reserves of the pool + /// @return _reserveToken0 The reserve of token0 + /// @return _reserveToken1 The reserve of token1 + function getReserves() external view returns (uint256 _reserveToken0, uint256 _reserveToken1) { + _reserveToken0 = reserveToken0; + _reserveToken1 = reserveToken1; + } + + /// @notice Get the price of the token in the pool + /// @param _tokenIn The token to get the price of + /// @param _tokenOut The token to get the price of + /// @return _price The price of the token in the pool + function getPrice(address _tokenIn, address _tokenOut) external view returns (uint256 _price) { + uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1; + uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1; + uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals; + uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals; + + _price = (_reserveTokenIn * 10 ** _tokenInDecimals) / (_reserveTokenOut * 10 ** _tokenOutDecimals); + } + + /// @notice Swap tokens in the pool + /// @param _sender The address of the sender + /// @param _tokenIn The token to swap in + /// @param _tokenOut The token to swap out + /// @param _amountIn The amount of tokens to swap in + /// @param _minAmountOut The minimum amount of tokens to swap out + function swap( + address _sender, + address _tokenIn, + address _tokenOut, + uint256 _amountIn, + uint256 _minAmountOut + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_tokenIn != token0 && _tokenIn != token1 || _tokenOut != token0 && _tokenOut != token1 || _tokenIn == _tokenOut) { + revert InvalidTokenPair(_tokenIn, _tokenOut); + } + + address _msgSender = msg.sender == address(eip712Swap) ? _sender : msg.sender; + + if (IERC20(_tokenIn).allowance(_msgSender, address(this)) < _amountIn) revert InsufficientAllowance(); + + uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1; + uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1; + uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals; + uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals; + + uint256 amountOut = (_amountIn * (10 ** _tokenOutDecimals) * _reserveTokenOut) / (_reserveTokenIn + _amountIn * (10 ** _tokenOutDecimals)); + uint256 _fee = feeManager.getFee(SwapParams(_tokenIn, _tokenOut, _amountIn, _reserveTokenIn, _reserveTokenOut)); + amountOut -= (_fee * (10 ** _tokenOutDecimals)) / (10 ** _tokenInDecimals); + + if (amountOut < _minAmountOut) revert InsufficientOutputAmount(_minAmountOut, amountOut); + if (amountOut > _reserveTokenOut) revert InsufficientLiquidity(); + + require(IERC20(_tokenIn).transferFrom(_msgSender, address(this), _amountIn)); + require(IERC20(_tokenOut).transfer(_msgSender, amountOut)); + + if (_tokenIn == token0) { + reserveToken0 += _amountIn; + reserveToken1 -= amountOut; + } else { + reserveToken1 += _amountIn; + reserveToken0 -= amountOut; + } + + emit Swap(_tokenIn, _tokenOut, _amountIn, amountOut); + } +} \ No newline at end of file From 218762a3e430a0ceed36e9a8da156aad7b097330 Mon Sep 17 00:00:00 2001 From: nicknotknack Date: Tue, 17 Jun 2025 10:38:38 +0400 Subject: [PATCH 3/3] feat: MIT license added --- src/AccessManager.sol | 4 +- src/EIP712Swap.sol | 78 ++++++++----- src/FeeManager.sol | 3 +- src/ISwap.sol | 67 ++++++------ src/LiquidityPool.sol | 247 ++++++++++++++++++++++-------------------- src/Roles.sol | 3 +- src/VaultMultisig.sol | 26 ++--- 7 files changed, 230 insertions(+), 198 deletions(-) diff --git a/src/AccessManager.sol b/src/AccessManager.sol index 1e2aed7..8b6e6dd 100644 --- a/src/AccessManager.sol +++ b/src/AccessManager.sol @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./Roles.sol"; + contract AccessManager is AccessControl { using Roles for bytes32; @@ -21,4 +23,4 @@ contract AccessManager is AccessControl { function isMultisigAdmin(address _address) external view returns (bool) { return hasRole(Roles.MULTISIG_ADMIN_ROLE, _address); } -} \ No newline at end of file +} diff --git a/src/EIP712Swap.sol b/src/EIP712Swap.sol index 9161f57..86094bb 100644 --- a/src/EIP712Swap.sol +++ b/src/EIP712Swap.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; @@ -6,41 +7,62 @@ import "./LiquidityPool.sol"; import "./ISwap.sol"; contract EIP712Swap is EIP712 { - using ECDSA for bytes32; + using ECDSA for bytes32; - mapping(address => uint256) private _nonces; + mapping(address => uint256) private _nonces; - bytes32 private constant SWAP_TYPEHASH = - keccak256("SwapRequest(address pool,address sender,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 nonce,uint256 deadline)"); + bytes32 private constant SWAP_TYPEHASH = keccak256( + "SwapRequest(address pool,address sender,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 nonce,uint256 deadline)" + ); - error InvalidSignature(); - error ExpiredSwapRequest(); - error InvalidNonce(); + error InvalidSignature(); + error ExpiredSwapRequest(); + error InvalidNonce(); - constructor() EIP712("EIP712Swap", "1") {} + constructor() EIP712("EIP712Swap", "1") {} - function getDomainSeparator() public view returns (bytes32) { - return _domainSeparatorV4(); - } + function getDomainSeparator() public view returns (bytes32) { + return _domainSeparatorV4(); + } - function getNonce(address _sender) public view returns (uint256) { - return _nonces[_sender]; - } + function getNonce(address _sender) public view returns (uint256) { + return _nonces[_sender]; + } - function verify(ISwap.SwapRequest memory _swapRequest, bytes memory _signature) public view returns (bool) { - bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(SWAP_TYPEHASH, _swapRequest.pool, _swapRequest.sender, _swapRequest.tokenIn, _swapRequest.tokenOut, _swapRequest.amountIn, _swapRequest.minAmountOut, _swapRequest.nonce, _swapRequest.deadline))); - address signer = digest.recover(_signature); - return signer == _swapRequest.sender; - } + function verify(ISwap.SwapRequest memory _swapRequest, bytes memory _signature) public view returns (bool) { + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + SWAP_TYPEHASH, + _swapRequest.pool, + _swapRequest.sender, + _swapRequest.tokenIn, + _swapRequest.tokenOut, + _swapRequest.amountIn, + _swapRequest.minAmountOut, + _swapRequest.nonce, + _swapRequest.deadline + ) + ) + ); + address signer = digest.recover(_signature); + return signer == _swapRequest.sender; + } - function executeSwap(ISwap.SwapRequest memory _swapRequest, bytes memory _signature) public returns (bool) { - if (!verify(_swapRequest, _signature)) revert InvalidSignature(); - if (_swapRequest.deadline < block.timestamp) revert ExpiredSwapRequest(); - if (_swapRequest.nonce != _nonces[_swapRequest.sender]) revert InvalidNonce(); + function executeSwap(ISwap.SwapRequest memory _swapRequest, bytes memory _signature) public returns (bool) { + if (!verify(_swapRequest, _signature)) revert InvalidSignature(); + if (_swapRequest.deadline < block.timestamp) revert ExpiredSwapRequest(); + if (_swapRequest.nonce != _nonces[_swapRequest.sender]) revert InvalidNonce(); - _nonces[_swapRequest.sender]++; - LiquidityPool(_swapRequest.pool).swap(_swapRequest.sender, _swapRequest.tokenIn, _swapRequest.tokenOut, _swapRequest.amountIn, _swapRequest.minAmountOut); + _nonces[_swapRequest.sender]++; + LiquidityPool(_swapRequest.pool).swap( + _swapRequest.sender, + _swapRequest.tokenIn, + _swapRequest.tokenOut, + _swapRequest.amountIn, + _swapRequest.minAmountOut + ); - return true; - } -} \ No newline at end of file + return true; + } +} diff --git a/src/FeeManager.sol b/src/FeeManager.sol index fd64206..ad23eed 100644 --- a/src/FeeManager.sol +++ b/src/FeeManager.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; @@ -26,4 +27,4 @@ contract FeeManager is Initializable, AccessControlUpgradeable, UUPSUpgradeable function getFee(ISwap.SwapParams memory swapParams) external view returns (uint256) { return (swapParams.amount0 * fee) / FEE_DENOMINATOR; } -} \ No newline at end of file +} diff --git a/src/ISwap.sol b/src/ISwap.sol index 6808882..c42c389 100644 --- a/src/ISwap.sol +++ b/src/ISwap.sol @@ -1,37 +1,38 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; interface ISwap { - /// @notice Parameters for the swap - /// @param token0 The address of the first token - /// @param token1 The address of the second token - /// @param amount0 The amount of the first token to swap - /// @param reserveToken0 The reserve of the first token - /// @param reserveToken1 The reserve of the second token - struct SwapParams { - address token0; - address token1; - uint256 amount0; - uint256 reserveToken0; - uint256 reserveToken1; - } + /// @notice Parameters for the swap + /// @param token0 The address of the first token + /// @param token1 The address of the second token + /// @param amount0 The amount of the first token to swap + /// @param reserveToken0 The reserve of the first token + /// @param reserveToken1 The reserve of the second token + struct SwapParams { + address token0; + address token1; + uint256 amount0; + uint256 reserveToken0; + uint256 reserveToken1; + } - /// @notice Parameters for the swap request - /// @param pool The address of the pool - /// @param sender The address of the sender - /// @param tokenIn The address of the token to swap in - /// @param tokenOut The address of the token to swap out - /// @param amountIn The amount of the token to swap in - /// @param minAmountOut The minimum amount of the token to swap out - /// @param nonce The nonce of the swap - /// @param deadline The deadline of the swap - struct SwapRequest { - address pool; - address sender; - address tokenIn; - address tokenOut; - uint256 amountIn; - uint256 minAmountOut; - uint256 nonce; - uint256 deadline; - } -} \ No newline at end of file + /// @notice Parameters for the swap request + /// @param pool The address of the pool + /// @param sender The address of the sender + /// @param tokenIn The address of the token to swap in + /// @param tokenOut The address of the token to swap out + /// @param amountIn The amount of the token to swap in + /// @param minAmountOut The minimum amount of the token to swap out + /// @param nonce The nonce of the swap + /// @param deadline The deadline of the swap + struct SwapRequest { + address pool; + address sender; + address tokenIn; + address tokenOut; + uint256 amountIn; + uint256 minAmountOut; + uint256 nonce; + uint256 deadline; + } +} diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 8a629c5..dc06017 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -6,132 +7,140 @@ import "./FeeManager.sol"; import "./EIP712Swap.sol"; contract LiquidityPool is ISwap, FeeManager { - address public token0; - uint256 public token0Decimals; - address public token1; - uint256 public token1Decimals; - uint256 public reserveToken0; - uint256 public reserveToken1; - FeeManager public feeManager; - EIP712Swap public eip712Swap; - - // @notice Emitted when liquidity is added to the pool - /// @param _token The token that was added - /// @param _amount The amount of tokens that were added - event LiquidityAdded(address indexed _token, uint256 _amount); - - // @notice Emitted when a swap is executed - /// @param _tokenIn The token that was swapped in - /// @param _tokenOut The token that was swapped out - /// @param _amountIn The amount of tokens that were swapped in - /// @param _amountOut The amount of tokens that were swapped out - event Swap(address indexed _tokenIn, address indexed _tokenOut, uint256 _amountIn, uint256 _amountOut); - - error InsufficientTokenBalance(); - error InvalidTokenAddress(address _token); - error InvalidTokenPair(address _tokenIn, address _tokenOut); - error InsufficientLiquidity(); - error InsufficientOutputAmount(uint256 expected, uint256 actual); - error ExcessiveInputAmount(uint256 expected, uint256 actual); - error InsufficientAllowance(); - - constructor(address _token0, uint256 _token0Decimals, address _token1, uint256 _token1Decimals, address _feeManager, address _eip712Swap) { - token0 = _token0; - token0Decimals = _token0Decimals; - token1 = _token1; - token1Decimals = _token1Decimals; - feeManager = FeeManager(_feeManager); - eip712Swap = EIP712Swap(_eip712Swap); - } - - // @notice Add liquidity to the pool - /// @param _token The token to add liquidity for - /// @param _amount The amount of tokens to add - function addLiquidity(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_token == token0 || _token == token1) { - revert InvalidTokenAddress(_token); + address public token0; + uint256 public token0Decimals; + address public token1; + uint256 public token1Decimals; + uint256 public reserveToken0; + uint256 public reserveToken1; + FeeManager public feeManager; + EIP712Swap public eip712Swap; + + // @notice Emitted when liquidity is added to the pool + /// @param _token The token that was added + /// @param _amount The amount of tokens that were added + event LiquidityAdded(address indexed _token, uint256 _amount); + + // @notice Emitted when a swap is executed + /// @param _tokenIn The token that was swapped in + /// @param _tokenOut The token that was swapped out + /// @param _amountIn The amount of tokens that were swapped in + /// @param _amountOut The amount of tokens that were swapped out + event Swap(address indexed _tokenIn, address indexed _tokenOut, uint256 _amountIn, uint256 _amountOut); + + error InsufficientTokenBalance(); + error InvalidTokenAddress(address _token); + error InvalidTokenPair(address _tokenIn, address _tokenOut); + error InsufficientLiquidity(); + error InsufficientOutputAmount(uint256 expected, uint256 actual); + error ExcessiveInputAmount(uint256 expected, uint256 actual); + error InsufficientAllowance(); + + constructor( + address _token0, + uint256 _token0Decimals, + address _token1, + uint256 _token1Decimals, + address _feeManager, + address _eip712Swap + ) { + token0 = _token0; + token0Decimals = _token0Decimals; + token1 = _token1; + token1Decimals = _token1Decimals; + feeManager = FeeManager(_feeManager); + eip712Swap = EIP712Swap(_eip712Swap); } - if (IERC20(_token).balanceOf(address(msg.sender)) < _amount) { - revert InsufficientTokenBalance(); - } - - require(IERC20(_token).transferFrom(msg.sender, address(this), _amount)); - - if (_token == token0) { - reserveToken0 += _amount; - } else if (_token == token1) { - reserveToken1 += _amount; - } - - emit LiquidityAdded(_token, _amount); - } - - // @notice Get the reserves of the pool - /// @return _reserveToken0 The reserve of token0 - /// @return _reserveToken1 The reserve of token1 - function getReserves() external view returns (uint256 _reserveToken0, uint256 _reserveToken1) { - _reserveToken0 = reserveToken0; - _reserveToken1 = reserveToken1; - } - - /// @notice Get the price of the token in the pool - /// @param _tokenIn The token to get the price of - /// @param _tokenOut The token to get the price of - /// @return _price The price of the token in the pool - function getPrice(address _tokenIn, address _tokenOut) external view returns (uint256 _price) { - uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1; - uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1; - uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals; - uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals; - - _price = (_reserveTokenIn * 10 ** _tokenInDecimals) / (_reserveTokenOut * 10 ** _tokenOutDecimals); - } - - /// @notice Swap tokens in the pool - /// @param _sender The address of the sender - /// @param _tokenIn The token to swap in - /// @param _tokenOut The token to swap out - /// @param _amountIn The amount of tokens to swap in - /// @param _minAmountOut The minimum amount of tokens to swap out - function swap( - address _sender, - address _tokenIn, - address _tokenOut, - uint256 _amountIn, - uint256 _minAmountOut - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_tokenIn != token0 && _tokenIn != token1 || _tokenOut != token0 && _tokenOut != token1 || _tokenIn == _tokenOut) { - revert InvalidTokenPair(_tokenIn, _tokenOut); - } - - address _msgSender = msg.sender == address(eip712Swap) ? _sender : msg.sender; + // @notice Add liquidity to the pool + /// @param _token The token to add liquidity for + /// @param _amount The amount of tokens to add + function addLiquidity(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_token == token0 || _token == token1) { + revert InvalidTokenAddress(_token); + } - if (IERC20(_tokenIn).allowance(_msgSender, address(this)) < _amountIn) revert InsufficientAllowance(); + if (IERC20(_token).balanceOf(address(msg.sender)) < _amount) { + revert InsufficientTokenBalance(); + } - uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1; - uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1; - uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals; - uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals; + require(IERC20(_token).transferFrom(msg.sender, address(this), _amount)); - uint256 amountOut = (_amountIn * (10 ** _tokenOutDecimals) * _reserveTokenOut) / (_reserveTokenIn + _amountIn * (10 ** _tokenOutDecimals)); - uint256 _fee = feeManager.getFee(SwapParams(_tokenIn, _tokenOut, _amountIn, _reserveTokenIn, _reserveTokenOut)); - amountOut -= (_fee * (10 ** _tokenOutDecimals)) / (10 ** _tokenInDecimals); + if (_token == token0) { + reserveToken0 += _amount; + } else if (_token == token1) { + reserveToken1 += _amount; + } - if (amountOut < _minAmountOut) revert InsufficientOutputAmount(_minAmountOut, amountOut); - if (amountOut > _reserveTokenOut) revert InsufficientLiquidity(); + emit LiquidityAdded(_token, _amount); + } - require(IERC20(_tokenIn).transferFrom(_msgSender, address(this), _amountIn)); - require(IERC20(_tokenOut).transfer(_msgSender, amountOut)); + // @notice Get the reserves of the pool + /// @return _reserveToken0 The reserve of token0 + /// @return _reserveToken1 The reserve of token1 + function getReserves() external view returns (uint256 _reserveToken0, uint256 _reserveToken1) { + _reserveToken0 = reserveToken0; + _reserveToken1 = reserveToken1; + } - if (_tokenIn == token0) { - reserveToken0 += _amountIn; - reserveToken1 -= amountOut; - } else { - reserveToken1 += _amountIn; - reserveToken0 -= amountOut; + /// @notice Get the price of the token in the pool + /// @param _tokenIn The token to get the price of + /// @param _tokenOut The token to get the price of + /// @return _price The price of the token in the pool + function getPrice(address _tokenIn, address _tokenOut) external view returns (uint256 _price) { + uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1; + uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1; + uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals; + uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals; + + _price = (_reserveTokenIn * 10 ** _tokenInDecimals) / (_reserveTokenOut * 10 ** _tokenOutDecimals); } - emit Swap(_tokenIn, _tokenOut, _amountIn, amountOut); - } -} \ No newline at end of file + /// @notice Swap tokens in the pool + /// @param _sender The address of the sender + /// @param _tokenIn The token to swap in + /// @param _tokenOut The token to swap out + /// @param _amountIn The amount of tokens to swap in + /// @param _minAmountOut The minimum amount of tokens to swap out + function swap(address _sender, address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _minAmountOut) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + if ( + _tokenIn != token0 && _tokenIn != token1 || _tokenOut != token0 && _tokenOut != token1 + || _tokenIn == _tokenOut + ) { + revert InvalidTokenPair(_tokenIn, _tokenOut); + } + + address _msgSender = msg.sender == address(eip712Swap) ? _sender : msg.sender; + + if (IERC20(_tokenIn).allowance(_msgSender, address(this)) < _amountIn) revert InsufficientAllowance(); + + uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1; + uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1; + uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals; + uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals; + + uint256 amountOut = (_amountIn * (10 ** _tokenOutDecimals) * _reserveTokenOut) + / (_reserveTokenIn + _amountIn * (10 ** _tokenOutDecimals)); + uint256 _fee = feeManager.getFee(SwapParams(_tokenIn, _tokenOut, _amountIn, _reserveTokenIn, _reserveTokenOut)); + amountOut -= (_fee * (10 ** _tokenOutDecimals)) / (10 ** _tokenInDecimals); + + if (amountOut < _minAmountOut) revert InsufficientOutputAmount(_minAmountOut, amountOut); + if (amountOut > _reserveTokenOut) revert InsufficientLiquidity(); + + require(IERC20(_tokenIn).transferFrom(_msgSender, address(this), _amountIn)); + require(IERC20(_tokenOut).transfer(_msgSender, amountOut)); + + if (_tokenIn == token0) { + reserveToken0 += _amountIn; + reserveToken1 -= amountOut; + } else { + reserveToken1 += _amountIn; + reserveToken0 -= amountOut; + } + + emit Swap(_tokenIn, _tokenOut, _amountIn, amountOut); + } +} diff --git a/src/Roles.sol b/src/Roles.sol index 1b0c7af..6f15733 100644 --- a/src/Roles.sol +++ b/src/Roles.sol @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; library Roles { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant MULTISIG_ADMIN_ROLE = keccak256("MULTISIG_ADMIN_ROLE"); -} \ No newline at end of file +} diff --git a/src/VaultMultisig.sol b/src/VaultMultisig.sol index 6dca6f0..766cb81 100644 --- a/src/VaultMultisig.sol +++ b/src/VaultMultisig.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import "./AccessManager.sol"; @@ -30,10 +31,10 @@ contract VaultMultisig { } /// @notice The mapping of transfer IDs to transfer details - mapping (uint256 => Transfer) private transfers; + mapping(uint256 => Transfer) private transfers; /// @notice The mapping for verification that address is a signer - mapping (address => bool) private multiSigSigners; + mapping(address => bool) private multiSigSigners; /// @notice Checks that signers array is not empty error SignersArrayCannotBeEmpty(); @@ -107,11 +108,7 @@ contract VaultMultisig { /// @notice Initializes the multisig contract /// @param _signers The array of multisig signers /// @param _quorum The number of signatures required to execute a transaction - constructor( - address[] memory _signers, - uint256 _quorum, - address _accessManager - ) { + constructor(address[] memory _signers, uint256 _quorum, address _accessManager) { if (_signers.length == 0) revert SignersArrayCannotBeEmpty(); if (_quorum > _signers.length) revert QuorumGreaterThanSigners(); if (_quorum == 0) revert QuorumCannotBeZero(); @@ -193,7 +190,7 @@ contract VaultMultisig { uint256 balance = address(this).balance; if (transfer.amount >= balance) revert InsufficientBalance(balance, transfer.amount); - (bool success, ) = transfer.to.call{value: transfer.amount}(""); + (bool success,) = transfer.to.call{value: transfer.amount}(""); if (!success) revert TransferFailed(_transferId); transfer.executed = true; @@ -210,12 +207,11 @@ contract VaultMultisig { /// @return amount The amount of tokens to transfer /// @return approvals The number of approvals required to execute the transfer /// @return executed Whether the transfer has been executed - function getTransfer(uint256 _transferId) external view returns ( - address to, - uint256 amount, - uint256 approvals, - bool executed - ) { + function getTransfer(uint256 _transferId) + external + view + returns (address to, uint256 amount, uint256 approvals, bool executed) + { Transfer storage transfer = transfers[_transferId]; return (transfer.to, transfer.amount, transfer.approvals, transfer.executed); } @@ -234,4 +230,4 @@ contract VaultMultisig { function getTransferCount() external view returns (uint256) { return transfersCount; } -} \ No newline at end of file +}