From db6789c3f8ad774dc55253f0599e0f2f0833f76a Mon Sep 17 00:00:00 2001 From: Ignacio Piris <66640666+nachopiris@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:30:24 -0300 Subject: [PATCH] Add functions to RequireUtils module (#188) * forge install: openzeppelin-contracts v4.8.0 * add new functions to require utils * all tests passed * format fixing * remove temp files * fix other format * change test funcion name * remove unnecessary comments on tokens mocks --- .gitmodules | 2 +- contracts/interfaces/tokens/IERC1155.sol | 22 +++++ contracts/interfaces/tokens/IERC20.sol | 14 +++ contracts/interfaces/tokens/IERC721.sol | 18 ++++ contracts/mocks/ERC1155Mock.sol | 63 +++++++++++++ contracts/mocks/ERC20Mock.sol | 51 +++++++++++ contracts/mocks/ERC721Mock.sol | 86 ++++++++++++++++++ contracts/modules/utils/RequireUtils.sol | 75 ++++++++++++++++ foundry_test/modules/utils/RequireUtils.t.sol | 90 ++++++++++++++++++- remappings.txt | 2 +- 10 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 contracts/interfaces/tokens/IERC1155.sol create mode 100644 contracts/interfaces/tokens/IERC20.sol create mode 100644 contracts/interfaces/tokens/IERC721.sol create mode 100644 contracts/mocks/ERC1155Mock.sol create mode 100644 contracts/mocks/ERC20Mock.sol create mode 100644 contracts/mocks/ERC721Mock.sol diff --git a/.gitmodules b/.gitmodules index ec715285..b6622fb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/foundry-rs/forge-std [submodule "lib/foundry-huff"] path = lib/foundry-huff - url = https://github.com/huff-language/foundry-huff + url = https://github.com/huff-language/foundry-huff \ No newline at end of file diff --git a/contracts/interfaces/tokens/IERC1155.sol b/contracts/interfaces/tokens/IERC1155.sol new file mode 100644 index 00000000..aed3d719 --- /dev/null +++ b/contracts/interfaces/tokens/IERC1155.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IERC1155 { + function balanceOf(address account, uint256 id) external view returns (uint256); + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); + function setApprovalForAll(address operator, bool approved) external; + function isApprovedForAll(address account, address operator) external view returns (bool); + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; + + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + event URI(string value, uint256 indexed id); +} diff --git a/contracts/interfaces/tokens/IERC20.sol b/contracts/interfaces/tokens/IERC20.sol new file mode 100644 index 00000000..77002f92 --- /dev/null +++ b/contracts/interfaces/tokens/IERC20.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/contracts/interfaces/tokens/IERC721.sol b/contracts/interfaces/tokens/IERC721.sol new file mode 100644 index 00000000..db541065 --- /dev/null +++ b/contracts/interfaces/tokens/IERC721.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IERC721 { + function balanceOf(address owner) external view returns (uint256 balance); + function ownerOf(uint256 tokenId) external view returns (address owner); + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function getApproved(uint256 tokenId) external view returns (address operator); + function setApprovalForAll(address operator, bool approved) external; + function isApprovedForAll(address owner, address operator) external view returns (bool); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); +} diff --git a/contracts/mocks/ERC1155Mock.sol b/contracts/mocks/ERC1155Mock.sol new file mode 100644 index 00000000..49a7ea6b --- /dev/null +++ b/contracts/mocks/ERC1155Mock.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract ERC1155Mock { + string public name = 'Mock ERC1155 Token'; + string public symbol = 'MERC1155'; + address public owner; + + mapping(uint256 => mapping(address => uint256)) public balances; + mapping(address => mapping(address => bool)) public operatorApprovals; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, 'Only owner can mint'); + _; + } + + function balanceOf(address account, uint256 id) public view returns (uint256) { + return balances[id][account]; + } + + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view returns (uint256[] memory) { + require(accounts.length == ids.length, 'Accounts and ids length mismatch'); + + uint256[] memory batchBalances = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balances[ids[i]][accounts[i]]; + } + return batchBalances; + } + + function mint(address to, uint256 id, uint256 amount) public onlyOwner { + require(to != address(0), 'Cannot mint to zero address'); + + balances[id][to] += amount; + emit TransferSingle(msg.sender, address(0), to, id, amount); + } + + function safeTransferFrom(address from, address to, uint256 id, uint256 amount) public { + require(from == msg.sender || isApprovedForAll(from, msg.sender), 'Not approved to transfer'); + + require(balances[id][from] >= amount, 'Insufficient balance'); + balances[id][from] -= amount; + balances[id][to] += amount; + + emit TransferSingle(msg.sender, from, to, id, amount); + } + + function setApprovalForAll(address operator, bool approved) public { + operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + function isApprovedForAll(address account, address operator) public view returns (bool) { + return operatorApprovals[account][operator]; + } + + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + event ApprovalForAll(address indexed account, address indexed operator, bool approved); +} diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol new file mode 100644 index 00000000..b4136f10 --- /dev/null +++ b/contracts/mocks/ERC20Mock.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract ERC20Mock { + string public name = 'Mock ERC20 Token'; + string public symbol = 'MERC20'; + uint8 public decimals = 18; + uint256 public totalSupply; + mapping(address => uint256) public balances; + mapping(address => mapping(address => uint256)) public allowances; + + constructor(uint256 initialSupply) { + totalSupply = initialSupply; + balances[msg.sender] = initialSupply; + } + + function balanceOf(address account) public view returns (uint256) { + return balances[account]; + } + + function transfer(address recipient, uint256 amount) public returns (bool) { + require(balances[msg.sender] >= amount, 'Insufficient balance'); + balances[msg.sender] -= amount; + balances[recipient] += amount; + emit Transfer(msg.sender, recipient, amount); + return true; + } + + function allowance(address owner, address spender) public view returns (uint256) { + return allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public returns (bool) { + allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) { + require(balances[sender] >= amount, 'Insufficient balance'); + require(allowances[sender][msg.sender] >= amount, 'Allowance exceeded'); + balances[sender] -= amount; + balances[recipient] += amount; + allowances[sender][msg.sender] -= amount; + emit Transfer(sender, recipient, amount); + return true; + } + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000..bc8fb75d --- /dev/null +++ b/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract ERC721Mock { + string public name = 'Mock ERC721 Token'; + string public symbol = 'MERC721'; + uint256 public totalSupply; + address public owner; + + mapping(address => uint256) public balances; + mapping(uint256 => address) public owners; + mapping(address => mapping(address => bool)) public operatorApprovals; + mapping(uint256 => address) public tokenApprovals; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, 'Only owner can mint'); + _; + } + + function balanceOf(address _owner) public view returns (uint256) { + return balances[_owner]; + } + + function ownerOf(uint256 tokenId) public view returns (address) { + address tokenOwner = owners[tokenId]; + require(tokenOwner != address(0), 'Token does not exist'); + return tokenOwner; + } + + function mint(address to, uint256 tokenId) public onlyOwner { + require(to != address(0), 'Cannot mint to zero address'); + require(owners[tokenId] == address(0), 'Token already minted'); + + owners[tokenId] = to; + balances[to] += 1; + totalSupply += 1; + + emit Transfer(address(0), to, tokenId); + } + + function transferFrom(address from, address to, uint256 tokenId) public { + require(ownerOf(tokenId) == from, 'Not the owner of the token'); + require(to != address(0), 'Cannot transfer to zero address'); + + require( + msg.sender == from || getApproved(tokenId) == msg.sender || isApprovedForAll(from, msg.sender), + 'Not approved to transfer' + ); + + balances[from] -= 1; + balances[to] += 1; + owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + function approve(address to, uint256 tokenId) public { + address tokenOwner = ownerOf(tokenId); + require(to != tokenOwner, 'Cannot approve current owner'); + require(msg.sender == tokenOwner || isApprovedForAll(tokenOwner, msg.sender), 'Not approved'); + + tokenApprovals[tokenId] = to; + emit Approval(tokenOwner, to, tokenId); + } + + function getApproved(uint256 tokenId) public view returns (address) { + return tokenApprovals[tokenId]; + } + + function setApprovalForAll(address operator, bool approved) public { + operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + function isApprovedForAll(address _owner, address operator) public view returns (bool) { + return operatorApprovals[_owner][operator]; + } + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); +} diff --git a/contracts/modules/utils/RequireUtils.sol b/contracts/modules/utils/RequireUtils.sol index 957e2c94..002e7805 100644 --- a/contracts/modules/utils/RequireUtils.sol +++ b/contracts/modules/utils/RequireUtils.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.18; import "../commons/ModuleNonce.sol"; import "../commons/submodules/nonce/SubModuleNonce.sol"; +import "../../interfaces/tokens/IERC20.sol"; +import "../../interfaces/tokens/IERC721.sol"; +import "../../interfaces/tokens/IERC1155.sol"; contract RequireUtils { /** @@ -28,4 +31,76 @@ contract RequireUtils { uint256 currentNonce = ModuleNonce(_wallet).readNonce(space); require(currentNonce >= nonce, "RequireUtils#requireMinNonce: NONCE_BELOW_REQUIRED"); } + + /** + * @notice Validates that a wallet has a minimum ERC20 token balance + * @param _token ERC20 token address + * @param _wallet Sequence wallet + * @param _minBalance Minimum required balance + */ + function requireMinERC20Balance(address _token, address _wallet, uint256 _minBalance) external view { + uint256 balance = IERC20(_token).balanceOf(_wallet); + require(balance >= _minBalance, 'RequireUtils#requireMinERC20Balance: BALANCE_TOO_LOW'); + } + + /** + * @notice Validates that a wallet has a minimum ERC20 allowance for a spender + * @param _token ERC20 token address + * @param _owner Sequence wallet + * @param _spender Address allowed to spend the tokens + * @param _minAllowance Minimum required allowance + */ + function requireMinERC20Allowance(address _token, address _owner, address _spender, uint256 _minAllowance) external view { + uint256 allowance = IERC20(_token).allowance(_owner, _spender); + require(allowance >= _minAllowance, 'RequireUtils#requireMinERC20Allowance: ALLOWANCE_TOO_LOW'); + } + + /** + * @notice Validates that a wallet owns a specific ERC721 token + * @param _token ERC721 token address + * @param _wallet Sequence wallet + * @param _tokenId Token ID to check for ownership + */ + function requireERC721Ownership(address _token, address _wallet, uint256 _tokenId) external view { + address owner = IERC721(_token).ownerOf(_tokenId); + require(owner == _wallet, 'RequireUtils#requireERC721Ownership: NOT_OWNER'); + } + + /** + * @notice Validates that an ERC721 token is approved for a specific spender + * @param _token ERC721 token address + * @param _owner Sequence wallet + * @param _spender Address that should have approval + * @param _tokenId Token ID to check for approval + */ + function requireERC721Approval(address _token, address _owner, address _spender, uint256 _tokenId) external view { + address approved = IERC721(_token).getApproved(_tokenId); + require( + approved == _spender || IERC721(_token).isApprovedForAll(_owner, _spender), + 'RequireUtils#requireERC721Approval: NOT_APPROVED' + ); + } + + /** + * @notice Validates that a wallet has a minimum balance of an ERC1155 token + * @param _token ERC1155 token address + * @param _wallet Sequence wallet + * @param _tokenId Token ID to check + * @param _minBalance Minimum required balance + */ + function requireMinERC1155Balance(address _token, address _wallet, uint256 _tokenId, uint256 _minBalance) external view { + uint256 balance = IERC1155(_token).balanceOf(_wallet, _tokenId); + require(balance >= _minBalance, 'RequireUtils#requireMinERC1155Balance: BALANCE_TOO_LOW'); + } + + /** + * @notice Validates that an ERC1155 token is approved for a specific operator + * @param _token ERC1155 token address + * @param _owner Sequence wallet + * @param _operator Address that should have operator approval + */ + function requireERC1155Approval(address _token, address _owner, address _operator) external view { + bool isApproved = IERC1155(_token).isApprovedForAll(_owner, _operator); + require(isApproved, 'RequireUtils#requireERC1155Approval: NOT_APPROVED'); + } } diff --git a/foundry_test/modules/utils/RequireUtils.t.sol b/foundry_test/modules/utils/RequireUtils.t.sol index 66acdbb4..5a507207 100644 --- a/foundry_test/modules/utils/RequireUtils.t.sol +++ b/foundry_test/modules/utils/RequireUtils.t.sol @@ -5,6 +5,10 @@ import "contracts/Factory.sol"; import "contracts/modules/commons/ModuleCalls.sol"; import "contracts/modules/utils/RequireUtils.sol"; +import "contracts/mocks/ERC20Mock.sol"; +import "contracts/mocks/ERC721Mock.sol"; +import "contracts/mocks/ERC1155Mock.sol"; + import "foundry_test/base/AdvTest.sol"; contract ModuleCallsImp is ModuleCalls { @@ -42,15 +46,21 @@ contract ModuleCallsImp is ModuleCalls { } } -contract SubModuleNonceTest is AdvTest { +contract RequireUtilsTest is AdvTest { ModuleCallsImp private imp; RequireUtils private requireUtils; + ERC20Mock private erc20; + ERC721Mock private erc721; + ERC1155Mock private erc1155; function setUp() external { requireUtils = new RequireUtils(); ModuleCallsImp template = new ModuleCallsImp(); Factory factory = new Factory(); imp = ModuleCallsImp(factory.deploy(address(template), bytes32(0))); + erc20 = new ERC20Mock(1000 * 10 ** 18); + erc721 = new ERC721Mock(); + erc1155 = new ERC1155Mock(); } function test_requireNonExpired(uint256 _expiration) external { @@ -74,4 +84,80 @@ contract SubModuleNonceTest is AdvTest { uint256 encoded = abi.decode(abi.encodePacked(_space, _nonce), (uint256)); requireUtils.requireMinNonce(address(imp), encoded); } -} + + function test_requireMinERC20Balance(uint256 _minBalance) external { + uint256 balance = erc20.balanceOf(address(this)); + + if (balance < _minBalance) { + vm.expectRevert(bytes('RequireUtils#requireMinERC20Balance: BALANCE_TOO_LOW')); + } + requireUtils.requireMinERC20Balance(address(erc20), address(this), _minBalance); + } + + function test_requireMinERC20Allowance(uint256 _minAllowance) external { + erc20.approve(address(imp), 100 * 10 ** 18); + + uint256 allowance = erc20.allowance(address(this), address(imp)); + + if (allowance < _minAllowance) { + vm.expectRevert(bytes('RequireUtils#requireMinERC20Allowance: ALLOWANCE_TOO_LOW')); + } + requireUtils.requireMinERC20Allowance(address(erc20), address(this), address(imp), _minAllowance); + } + + function test_requireERC721Ownership(uint256 _tokenId) external { + if (_tokenId % 2 == 0) { + erc721.mint(address(imp), _tokenId); + } else { + erc721.mint(address(this), _tokenId); + } + + if (erc721.ownerOf(_tokenId) != address(this)) { + vm.expectRevert(bytes('RequireUtils#requireERC721Ownership: NOT_OWNER')); + } + requireUtils.requireERC721Ownership(address(erc721), address(this), _tokenId); + } + + function test_requireERC721Approval(uint256 _tokenId) external { + erc721.mint(address(this), _tokenId); + + if (_tokenId % 2 == 0) { + erc721.approve(address(imp), _tokenId); + } + + if (_tokenId % 5 == 0) { + erc721.setApprovalForAll(address(imp), true); + } + + address approved = erc721.getApproved(_tokenId); + + if (approved != address(imp) && !erc721.isApprovedForAll(address(this), address(imp))) { + vm.expectRevert(bytes('RequireUtils#requireERC721Approval: NOT_APPROVED')); + } + requireUtils.requireERC721Approval(address(erc721), address(this), address(imp), _tokenId); + } + + function test_requireMinERC1155Balance(uint256 _tokenId, uint256 _minBalance) external { + if (_tokenId % 2 == 0) { + erc1155.mint(address(this), _tokenId, _minBalance); + } + + uint256 balance = erc1155.balanceOf(address(this), _tokenId); + + if (balance < _minBalance) { + vm.expectRevert(bytes('RequireUtils#requireMinERC1155Balance: BALANCE_TOO_LOW')); + } + requireUtils.requireMinERC1155Balance(address(erc1155), address(this), _tokenId, _minBalance); + } + + function test_requireERC1155Approval(uint256 _tokenId) external { + if (_tokenId % 2 == 0) { + erc1155.setApprovalForAll(address(imp), true); + } + + if (!erc1155.isApprovedForAll(address(this), address(imp))) { + vm.expectRevert(bytes('RequireUtils#requireERC1155Approval: NOT_APPROVED')); + } + requireUtils.requireERC1155Approval(address(erc1155), address(this), address(imp)); + } +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 845bd0af..1076a425 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ +forge-std/=lib/forge-std/src/ \ No newline at end of file