diff --git a/.env.sample b/.env.sample index 7d6c4a9..77d350c 100644 --- a/.env.sample +++ b/.env.sample @@ -3,12 +3,13 @@ SWAPLACE_ADDRESS=0x000000000000000000000000000000000000000000 # Mocks last deployed contracts addresses. ERC20_ADDRESS=0x000000000000000000000000000000000000000000 ERC721_ADDRESS=0x000000000000000000000000000000000000000000 +ERC1155_ADDRESS=0x000000000000000000000000000000000000000000 # Amount of ERC20 tokens to be minted for signer address. AMOUNT=1000 # Token ID of ERC721 to be minted for signer address. TOKEN_ID=1 # The swap to be accepted by the acceptee. -SWAP_ID=3 +SWAP_ID=1 # These are public known private keys and are here as an example. # You should change the private keys to your own private keys. @@ -28,6 +29,7 @@ SEPOLIA_RPC_URL=https://ethereum-sepolia.publicnode.com MUMBAI_RPC_URL=https://polygon-mumbai-bor.publicnode.com FUJI_RPC_URL=https://avalanche-fuji-c-chain.publicnode.com BNB_TESTNET_RPC_URL=https://bsc-testnet.publicnode.com +KAKAROT_SEPOLIA_RPC_URL=https://sepolia-rpc.kakarot.org # MAINNETS ETH_RPC_URL="https://eth.llamarpc.com" diff --git a/contracts/SwapFactory.sol b/contracts/SwapFactory.sol index 5ec37fd..4e039fc 100644 --- a/contracts/SwapFactory.sol +++ b/contracts/SwapFactory.sol @@ -18,19 +18,22 @@ import {ISwapFactory} from "./interfaces/ISwapFactory.sol"; * - The `allowed` address is the address that can accept the Swap. If the allowed * address is the zero address, then anyone can accept the Swap. * - The `expiry` date is the timestamp that the Swap will be available to accept. + * - The `recipient` is the address that will receive the ETH as type uint8. If the + * recipient is equals to 0, the acceptee will receive the ETH. If the recipient is + * between 1<>255 then the recipient will be the owner of the Swap. + * - The `value` is the amount of ETH that the recipient will receive with a maximum + * of 6 decimals (0.000001 ETH). The contract will fill the value up to 18 decimals. * - The `biding` are the assets that the owner is offering. * - The `asking` are the assets that the owner wants in exchange. * * The Swap struct uses an {Asset} struct to represent the asset. This struct is * composed of: * - * - The `address` of the asset. This address can be from an ERC20 or ERC721 contract. + * - The `address` of the token asset. * - The `amount` or `id` of the asset. This amount can be the amount of ERC20 tokens - * or the ID of an ERC721 token. - * - * To use other standards, like ERC1155, you can wrap the ownership of the asset - * in an a trusted contract and Swap as an ERC721. This way, you can tokenize any - * on-chain execution and trade on Swaplace. + * or the NFT ID of an ERC721. + * - The `amount` and `id` can be encoded together in a single uint256, allowing the + * ERC1155 tokens to be swapped. */ abstract contract SwapFactory is ISwapFactory, ISwap, IErrors { /** @@ -43,39 +46,91 @@ abstract contract SwapFactory is ISwapFactory, ISwap, IErrors { return Asset(addr, amountOrId); } + /** + * @dev See {ISwapFactory-make1155Asset}. + */ + function make1155Asset( + address addr, + uint120 tokenId, + uint120 tokenAmount + ) public pure virtual returns (Asset memory) { + return Asset(addr, encodeAsset(tokenId, tokenAmount)); + } + /** * @dev See {ISwapFactory-makeSwap}. */ function makeSwap( address owner, address allowed, - uint256 expiry, + uint32 expiry, + uint8 recipient, + uint56 value, Asset[] memory biding, Asset[] memory asking ) public view virtual returns (Swap memory) { - if (expiry < block.timestamp) revert InvalidExpiry(expiry); - - if (biding.length == 0 || asking.length == 0) revert InvalidAssetsLength(); + if (expiry < block.timestamp) revert InvalidExpiry(); + uint256 config = encodeConfig(allowed, expiry, recipient, value); + return Swap(owner, config, biding, asking); + } - uint256 config = packData(allowed, expiry); + /** + * @dev See {ISwapFactory-encodeAsset}. + */ + function encodeAsset( + uint120 tokenId, + uint120 tokenAmount + ) public pure returns (uint256 amountAndId) { + return + (uint256(type(uint16).max) << 240) | + (uint256(tokenId) << 120) | + uint256(tokenAmount); + } - return Swap(owner, config, biding, asking); + /** + * @dev See {ISwapFactory-decodeAsset}. + */ + function decodeAsset( + uint256 amountOrId + ) + public + pure + returns (uint16 tokenType, uint256 tokenId, uint256 tokenAmount) + { + return ( + uint16(amountOrId >> 240), + uint256(uint120(amountOrId >> 120)), + uint256(uint120(amountOrId)) + ); } /** - * @dev See {ISwapFactory-packData}. + * @dev See {ISwapFactory-encodeConfig}. */ - function packData( + function encodeConfig( address allowed, - uint256 expiry + uint32 expiry, + uint8 recipient, + uint56 value ) public pure returns (uint256) { - return (uint256(uint160(allowed)) << 96) | uint256(expiry); + return + (uint256(uint160(allowed)) << 96) | + (uint256(expiry) << 64) | + (uint256(recipient) << 56) | + uint256(value); } /** - * @dev See {ISwapFactory-parseData}. + * @dev See {ISwapFactory-decodeConfig}. */ - function parseData(uint256 config) public pure returns (address, uint256) { - return (address(uint160(config >> 96)), uint256(config & ((1 << 96) - 1))); + function decodeConfig( + uint256 config + ) public pure returns (address, uint32, uint8, uint56) { + return ( + address(uint160(config >> 96)), + uint32(config >> 64), + uint8(config >> 56), + uint56(config) + ); } } diff --git a/contracts/Swaplace.sol b/contracts/Swaplace.sol index 901dfb5..444858e 100644 --- a/contracts/Swaplace.sol +++ b/contracts/Swaplace.sol @@ -3,15 +3,14 @@ pragma solidity ^0.8.17; import {IERC165} from "./interfaces/IERC165.sol"; import {ISwaplace} from "./interfaces/ISwaplace.sol"; -import {ITransfer} from "./interfaces/ITransfer.sol"; import {SwapFactory} from "./SwapFactory.sol"; /** * @author @0xneves | @blockful_io - * @dev Swaplace is a Decentralized Feeless DEX. It has no owners, it cannot be stopped. - * Its cern is to facilitate swaps between virtual assets following the ERC standard. + * @dev Swaplace is a decentralized and feeless DEX/OTC. Ownerless, it cannot be stopped. + * It's core is to facilitate swaps between virtual assets using the ERC standard. * Users can propose or accept swaps by allowing Swaplace to move their assets using the - * `approve` or `permit` function. + * `approve`, `permit` or similar functions. */ contract Swaplace is SwapFactory, ISwaplace, IERC165 { /// @dev Swap Identifier counter. @@ -20,21 +19,42 @@ contract Swaplace is SwapFactory, ISwaplace, IERC165 { /// @dev Mapping of Swap ID to Swap struct. See {ISwap-Swap}. mapping(uint256 => Swap) private _swaps; + /** + * @dev See {ISwaplace-getSwap}. + */ + function getSwap(uint256 swapId) public view returns (Swap memory) { + return _swaps[swapId]; + } + + /** + * @dev Getter function for _totalSwaps. + */ + function totalSwaps() public view returns (uint256) { + return _totalSwaps; + } + /** * @dev See {ISwaplace-createSwap}. */ - function createSwap(Swap calldata swap) public returns (uint256) { - if (swap.owner != msg.sender) revert InvalidAddress(msg.sender); + function createSwap(Swap calldata swap) public payable returns (uint256) { + if (swap.owner != msg.sender) revert InvalidAddress(); assembly { sstore(_totalSwaps.slot, add(sload(_totalSwaps.slot), 1)) } uint256 swapId = _totalSwaps; - _swaps[swapId] = swap; - (address allowed, ) = parseData(swap.config); + (address allowed, , uint8 recipient, uint256 value) = decodeConfig( + swap.config + ); + + if (value > 0) { + if (recipient == 0) { + if (value * 1e12 != msg.value) revert InvalidValue(); + } else if (msg.value > 0) revert InvalidValue(); + } emit SwapCreated(swapId, msg.sender, allowed); @@ -44,43 +64,31 @@ contract Swaplace is SwapFactory, ISwaplace, IERC165 { /** * @dev See {ISwaplace-acceptSwap}. */ - function acceptSwap(uint256 swapId, address receiver) public returns (bool) { + function acceptSwap( + uint256 swapId, + address receiver + ) public payable returns (bool) { Swap memory swap = _swaps[swapId]; - (address allowed, uint256 expiry) = parseData(swap.config); - - if (allowed != address(0) && allowed != msg.sender) - revert InvalidAddress(msg.sender); - - if (expiry < block.timestamp) revert InvalidExpiry(expiry); + ( + address allowed, + uint32 expiry, + uint8 recipient, + uint256 value + ) = decodeConfig(swap.config); + if (allowed != address(0) && allowed != msg.sender) revert InvalidAddress(); + if (expiry < block.timestamp) revert InvalidExpiry(); _swaps[swapId].config = 0; - Asset[] memory assets = swap.asking; + _transferFrom(msg.sender, swap.owner, swap.asking); + _transferFrom(swap.owner, receiver, swap.biding); - for (uint256 i = 0; i < assets.length; ) { - ITransfer(assets[i].addr).transferFrom( - msg.sender, - swap.owner, - assets[i].amountOrId - ); - assembly { - i := add(i, 1) - } - } - - assets = swap.biding; - - for (uint256 i = 0; i < assets.length; ) { - ITransfer(assets[i].addr).transferFrom( - swap.owner, - receiver, - assets[i].amountOrId - ); - assembly { - i := add(i, 1) - } - } + if (value > 0) + if (recipient == 0) _payNativeEth(receiver, value * 1e12); + else if (recipient > 0 && value * 1e12 == msg.value) + _payNativeEth(swap.owner, value * 1e12); + else revert InvalidValue(); emit SwapAccepted(swapId, swap.owner, msg.sender); @@ -91,22 +99,65 @@ contract Swaplace is SwapFactory, ISwaplace, IERC165 { * @dev See {ISwaplace-cancelSwap}. */ function cancelSwap(uint256 swapId) public { - if (_swaps[swapId].owner != msg.sender) revert InvalidAddress(msg.sender); - - (, uint256 expiry) = parseData(_swaps[swapId].config); + Swap memory swap = _swaps[swapId]; + if (swap.owner != msg.sender) revert InvalidAddress(); - if (expiry < block.timestamp) revert InvalidExpiry(expiry); + (, uint32 expiry, uint8 recipient, uint256 value) = decodeConfig( + swap.config + ); + if (expiry < block.timestamp && (value == 0 || recipient > 0)) { + revert InvalidExpiry(); + } _swaps[swapId].config = 0; + if (value > 0 && recipient == 0) { + _payNativeEth(msg.sender, value * 1e12); + } + emit SwapCanceled(swapId, msg.sender); } /** - * @dev See {ISwaplace-getSwap}. + * @dev Send an amount of native Ether to the receiver address. */ - function getSwap(uint256 swapId) public view returns (Swap memory) { - return _swaps[swapId]; + function _payNativeEth(address receiver, uint256 value) internal { + (bool success, ) = receiver.call{value: value}(""); + if (!success) revert InvalidValue(); + } + + /** + * @dev Transfer multiple 'assets' from 'from' to 'to'. + * + * `0x23b872dd` - Selector of the `transferFrom` function (ERC20, ERC721). + * `0xf242432a` - Selector of the `safeTransferFrom` function (ERC1155). + */ + function _transferFrom( + address from, + address to, + Asset[] memory assets + ) internal { + for (uint256 i; i < assets.length; ) { + (uint16 assetType, uint256 tokenId, uint256 tokenAmount) = decodeAsset( + assets[i].amountOrId + ); + + if (assetType == type(uint16).max) { + (bool success, ) = address(assets[i].addr).call( + abi.encodeWithSelector(0xf242432a, from, to, tokenId, tokenAmount, "") + ); + if (!success) revert InvalidCall(); + } else { + (bool success, ) = address(assets[i].addr).call( + abi.encodeWithSelector(0x23b872dd, from, to, assets[i].amountOrId) + ); + if (!success) revert InvalidCall(); + } + + assembly { + i := add(i, 1) + } + } } /** @@ -119,11 +170,4 @@ contract Swaplace is SwapFactory, ISwaplace, IERC165 { interfaceID == type(IERC165).interfaceId || interfaceID == type(ISwaplace).interfaceId; } - - /** - * @dev Getter function for _totalSwaps. - */ - function totalSwaps() public view returns (uint256) { - return _totalSwaps; - } } diff --git a/contracts/echidna/TestSwapFactory.sol b/contracts/echidna/TestSwapFactory.sol index b8ff11b..931c506 100644 --- a/contracts/echidna/TestSwapFactory.sol +++ b/contracts/echidna/TestSwapFactory.sol @@ -32,12 +32,14 @@ contract TestFactory is SwapFactory { Swap memory swap = makeSwap( owner, address(0), - block.timestamp + 1000, + uint32(block.timestamp + 1000), + 0, + 0, make_asset_array(addr, amountOrId), make_asset_array(addr, amountOrId) ); - (, uint256 expiry) = parseData(swap.config); + (, uint32 expiry, , ) = decodeConfig(swap.config); assert(expiry > block.timestamp); assert(swap.biding.length > 0); @@ -46,16 +48,14 @@ contract TestFactory is SwapFactory { } function echidna_revert_invalid_expiry() public view { - makeSwap(address(0), address(0), block.timestamp - 100, _asset, _asset); - } - - function echidna_revert_invalid_length() public view { makeSwap( address(0), address(0), - block.timestamp + 100, - new Asset[](0), - new Asset[](0) + uint32(block.timestamp - 100), + 0, + 0, + _asset, + _asset ); } } diff --git a/contracts/interfaces/IErrors.sol b/contracts/interfaces/IErrors.sol index 25e3f99..1395042 100644 --- a/contracts/interfaces/IErrors.sol +++ b/contracts/interfaces/IErrors.sol @@ -8,20 +8,20 @@ interface IErrors { /** * @dev Displayed when the caller is not the owner of the swap. */ - error InvalidAddress(address caller); + error InvalidAddress(); /** - * @dev Displayed when the amount of {ISwap-Asset} has a length of zero. - * - * NOTE: The `biding` or `asking` array must not be empty to avoid mistakes - * when creating a swap. Assuming one side of the swap is empty, the - * correct approach should be the usage of {transferFrom} and we reinforce - * this behavior by requiring the length of the array to be bigger than zero. + * @dev Displayed when the `expiry` date is in the past. */ - error InvalidAssetsLength(); + error InvalidExpiry(); /** - * @dev Displayed when the `expiry` date is in the past. + * @dev Displayed when the `msg.value` doesn't match the swap request. + */ + error InvalidValue(); + + /** + * @dev Displayed when a low level call failed to execute. */ - error InvalidExpiry(uint256 timestamp); + error InvalidCall(); } diff --git a/contracts/interfaces/ISwap.sol b/contracts/interfaces/ISwap.sol index 693f162..c1fd5b7 100644 --- a/contracts/interfaces/ISwap.sol +++ b/contracts/interfaces/ISwap.sol @@ -23,11 +23,14 @@ interface ISwap { * @dev The Swap struct is the heart of Swaplace. * * It is composed of: - * - `owner` of the Swap. - * - `config` represents two packed values: 'allowed' for the allowed address - * to accept the swap and 'expiry' for the expiration date of the swap. - * - `biding` assets that are being bided by the owner. - * - `asking` assets that are being asked by the owner. + * - `owner` creator of the Swap. + * - `config` configuration of four packed values: + * - - `allowed` for the allowed address to accept the swap. + * - - `expiry` for the expiration date of the swap in unix time. + * - - `recipient` for the address that will receive the ETH. + * - - `value` for the amount of ETH that the recipient will receive. + * - `biding` assets offered by the swap creator. + * - `asking` assets asked by the swap creator. * * NOTE: When `allowed` address is the zero address, anyone can accept the Swap. */ diff --git a/contracts/interfaces/ISwapFactory.sol b/contracts/interfaces/ISwapFactory.sol index 70a3d43..a975ce6 100644 --- a/contracts/interfaces/ISwapFactory.sol +++ b/contracts/interfaces/ISwapFactory.sol @@ -8,42 +8,125 @@ import {ISwap} from "./ISwap.sol"; */ interface ISwapFactory { /** - * @dev Constructs an asset struct that works for ERC20 or ERC721. - * This function is a utility to easily create an `Asset` struct on-chain or off-chain. + * @dev Make an {ISwap-Asset} struct to work with token standards. + * + * @param addr is the address of the token asset. + * @param amountOrId is the amount of tokens or the ID of the NFT. */ function makeAsset( address addr, uint256 amountOrId ) external pure returns (ISwap.Asset memory); + /** + * @dev Make an {ISwap-Asset} struct to work with token standards. + * + * NOTE: Different from the {makeAsset} function, this function is used to + * encode the token ID and token amount into a single uint256. This is made + * to work with the ERC1155 standard. + * + * @param addr is the address of the token asset. + * @param tokenId is the ID of the ERC1155 token. + * @param tokenAmount is the amount of the ERC1155 token. + */ + function make1155Asset( + address addr, + uint120 tokenId, + uint120 tokenAmount + ) external pure returns (ISwap.Asset memory); + /** * @dev Build a swap struct to use in the {Swaplace-createSwap} function. * * Requirements: * - * - `expiry` cannot be in the past timestamp. - * - `biding` and `asking` cannot be empty. + * - `expiry` cannot be in the past. + * + * @param owner is the address that created the Swap. + * @param allowed is the address that can accept the Swap. If the allowed + * address is the zero address, then anyone can accept the Swap. + * @param expiry is the timestamp that the Swap will be available to accept. + * @param recipient is the address that will receive the ETH. `0` for the acceptee + * and `1<>255` for the owner. + * @param value is the amount of ETH that the recipient will receive. Maximum of + * 6 decimals (0.000001 ETH). The contract will fill the value up to 18 decimals. */ function makeSwap( address owner, address allowed, - uint256 expiry, + uint32 expiry, + uint8 recipient, + uint56 value, ISwap.Asset[] memory assets, ISwap.Asset[] memory asking ) external view returns (ISwap.Swap memory); /** - * @dev Packs `allowed` and the `expiry`. - * This function returns the bitwise packing of `allowed` and `expiry` as a uint256. + * @dev Encode `tokenId` and `tokenAmount` into a single uint256 while adding a flag + * to indicate that it's an ERC1155 token. + * + * NOTE: The flag is set to 0xFFFFFFFF. + * + * @param tokenId is the ID of the ERC1155 token. + * @param tokenAmount is the amount of the ERC1155 token. */ - function packData( + function encodeAsset( + uint120 tokenId, + uint120 tokenAmount + ) external pure returns (uint256 amountAndId); + + /** + * @dev Decode `amountOrId` returning the first 4 bytes to try match with 0xFFFFFFFF. + * If the flag is set to 0xFFFFFFFF, then it's an ERC1155 standard, otherwise it's + * assumed to be an ERC20 or ERC721. + * + * NOTE: If it's an ERC1155 token, then the next 120 bits are the token ID and the next + * 120 bits are the token amount. + * + * WARNING: Swaplace cannot handle ERC1155 tokens where the ID or the amount is greater + * than 120 bits. + * + * @param amountAndId is the amount of tokens and the ID of the ERC1155 token. + * @return tokenType is the flag to indicate the token standard. + * @return tokenId is the ID of the ERC1155 token. + * @return tokenAmount is the amount of the ERC1155 token. + * + */ + function decodeAsset( + uint256 amountAndId + ) + external + pure + returns (uint16 tokenType, uint256 tokenId, uint256 tokenAmount); + + /** + * @dev This function uses bitwise to return an encoded uint256 of the following parameters. + * + * @param allowed address is the address that can accept the Swap. If the allowed + * address is the zero address, then anyone can accept the Swap. + * @param expiry date is the timestamp that the Swap will be available to accept. + * @param recipient is the address that will receive the ETH as type uint8. If the + * recipient is equals to 0, the acceptee will receive the ETH. If the recipient is + * between 1<>255 then the recipient will be the owner of the Swap. + * @param value is the amount of ETH that the recipient will receive with a maximum + * of 6 decimals (0.000001 ETH). The contract will fill the value up to 18 decimals. + */ + function encodeConfig( address allowed, - uint256 expiry - ) external pure returns (uint256); + uint32 expiry, + uint8 recipient, + uint56 value + ) external pure returns (uint256 config); /** - * @dev Parsing the `config`. - * This function returns the extracted values of `allowed` and `expiry`. + * @dev Decode `config` into their respective variables. + * + * @param config is the encoded uint256 configuration of the Swap. */ - function parseData(uint256 config) external pure returns (address, uint256); + function decodeConfig( + uint256 config + ) + external + pure + returns (address allowed, uint32 expiry, uint8 recipient, uint56 value); } diff --git a/contracts/interfaces/ISwaplace.sol b/contracts/interfaces/ISwaplace.sol index 5dedc9d..bc6f217 100644 --- a/contracts/interfaces/ISwaplace.sol +++ b/contracts/interfaces/ISwaplace.sol @@ -41,7 +41,9 @@ interface ISwaplace { * * Emits a {SwapCreated} event. */ - function createSwap(ISwap.Swap calldata Swap) external returns (uint256); + function createSwap( + ISwap.Swap calldata Swap + ) external payable returns (uint256); /** * @dev Accepts a Swap. Once the Swap is accepted, the expiry is set @@ -59,7 +61,10 @@ interface ISwaplace { * NOTE: The expiry is set to 0, because if the Swap is expired it * will revert, preventing reentrancy attacks. */ - function acceptSwap(uint256 swapId, address receiver) external returns (bool); + function acceptSwap( + uint256 swapId, + address receiver + ) external payable returns (bool); /** * @dev Cancels an active Swap by setting the expiry to zero. diff --git a/contracts/interfaces/ITransfer.sol b/contracts/interfaces/ITransfer.sol deleted file mode 100644 index cfde089..0000000 --- a/contracts/interfaces/ITransfer.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -/** - * @dev Generalized Interface for {IERC20} and {IERC721} `transferFrom` functions. - */ -interface ITransfer { - /** - * @dev See {IERC20-transferFrom} or {IERC721-transferFrom}. - * - * Moves an `amount` for ERC20 or `tokenId` for ERC721 from `from` to `to`. - * - * Emits a {Transfer} event. - */ - function transferFrom(address from, address to, uint256 amountOrId) external; -} diff --git a/contracts/mock/MockERC1155.sol b/contracts/mock/MockERC1155.sol new file mode 100644 index 0000000..b5904c0 --- /dev/null +++ b/contracts/mock/MockERC1155.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +contract MockERC1155 is ERC1155 { + using Strings for uint256; + + string private _name; + string private _symbol; + + function name() public view returns (string memory) { + return _name; + } + + function symbol() public view returns (string memory) { + return _symbol; + } + + constructor() + ERC1155("ipfs://QmQJnHseE9VPw5qVxuEhxTiZ7avzgkCdFz69rg86UvTZdk/") + { + _name = "MockERC1155"; + _symbol = "M1155"; + } + + function mint(address to, uint256 id, uint256 amount) public { + _mint(to, id, amount, ""); + } + + function tokenURI( + uint256 tokenId + ) public view virtual returns (string memory) { + return uri(tokenId); + } +} diff --git a/contracts/mock/MockERC721.sol b/contracts/mock/MockERC721.sol index e763dc5..80bbfc8 100644 --- a/contracts/mock/MockERC721.sol +++ b/contracts/mock/MockERC721.sol @@ -12,4 +12,10 @@ contract MockERC721 is ERC721 { totalSupply++; _mint(to, id); } -} + + function tokenURI( + uint256 + ) public view virtual override returns (string memory) { + return "ipfs://QmWodCkovJk18U75g8Veg6rCnw7951vQvTjYfS7J3nMFma/"; + } +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 513be85..5e9334d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -26,6 +26,10 @@ const config: HardhatUserConfig = { /** * @dev Testnets */ + kakarot: { + url: `${process.env.KAKAROT_SEPOLIA_RPC_URL}`, + accounts: [`${DEPLOYER_PRIVATE_KEY}`], + }, sepolia: { url: `${process.env.SEPOLIA_RPC_URL}`, accounts: [`${DEPLOYER_PRIVATE_KEY}`], diff --git a/scripts/approve.ts b/scripts/approve.ts index 5137afd..34faa3c 100755 --- a/scripts/approve.ts +++ b/scripts/approve.ts @@ -10,12 +10,18 @@ async function main() { /// @dev This is the list of mock deployments addresses that were stored in the `.env` file. const ERC20_ADDRESS = process.env.ERC20_ADDRESS || 0x0; const ERC721_ADDRESS = process.env.ERC721_ADDRESS || 0x0; + const ERC1155_ADDRESS = process.env.ERC1155_ADDRESS || 0x0; /// @dev The Swaplace address also needs to be instance to receive the approvals. const SWAPLACE_ADDRESS = process.env.SWAPLACE_ADDRESS || 0x0; /// @dev Will throw an error if any of the addresses were not set in the `.env` file. - if (!ERC20_ADDRESS || !ERC721_ADDRESS || !SWAPLACE_ADDRESS) { + if ( + !ERC20_ADDRESS || + !ERC721_ADDRESS || + !SWAPLACE_ADDRESS || + !ERC1155_ADDRESS + ) { throw new Error( - "Invalid ERC20, ERC721 or Swaplace address, please check if the addresse in the `.env` file is set up correctly.", + "Invalid ERC20, ERC721, ERC1155 or Swaplace address, please check if the addresse in the `.env` file is set up correctly.", ); } @@ -39,6 +45,7 @@ async function main() { /// @dev The returned contract instance that will be deployed via the deploy function in utils. let MockERC20: Contract; let MockERC721: Contract; + let MockERC1155: Contract; /// @dev will throw an error if any of the accounts was not set up correctly. try { @@ -63,6 +70,11 @@ async function main() { ERC721_ADDRESS, signers[0], ); + MockERC1155 = await ethers.getContractAt( + "MockERC1155", + ERC1155_ADDRESS, + signers[0], + ); } catch (error) { throw new Error( `Error deploying one of the Mock Contracts. @@ -76,11 +88,13 @@ async function main() { /// @dev Responses from the minting transactions. let txErc20; let txErc721; + let txErc1155; /// @dev We are approving the signer address to spend the amount of tokens. try { txErc20 = await MockERC20.approve(SWAPLACE_ADDRESS, amount); txErc721 = await MockERC721.approve(SWAPLACE_ADDRESS, tokenId); + txErc1155 = await MockERC1155.setApprovalForAll(SWAPLACE_ADDRESS, true); } catch (error) { throw new Error( `Error while approving the tokens. Make sure that the approve function is @@ -97,6 +111,7 @@ async function main() { tokenId, txErc721.hash, ); + console.log("\nERC1155 Approved all tokens \nAt Tx %s", txErc1155.hash); } main().catch((error) => { diff --git a/scripts/createSwap.ts b/scripts/createSwap.ts index a382b64..28de0a7 100644 --- a/scripts/createSwap.ts +++ b/scripts/createSwap.ts @@ -1,19 +1,30 @@ import { ethers } from "hardhat"; import { Contract } from "ethers"; import { blocktimestamp, storeEnv } from "../test/utils/utils"; -import { Swap, composeSwap } from "../test/utils/SwapFactory"; +import { + Swap, + composeSwap, + encodeAsset, + encodeConfig, +} from "../test/utils/SwapFactory"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; export async function main() { /// @dev This is the list of mock deployments addresses that were stored in the `.env` file. const ERC20_ADDRESS = process.env.ERC20_ADDRESS || 0x0; const ERC721_ADDRESS = process.env.ERC721_ADDRESS || 0x0; + const ERC1155_ADDRESS = process.env.ERC1155_ADDRESS || 0x0; /// @dev The Swaplace address also needs to be instance to receive the approvals. const SWAPLACE_ADDRESS = process.env.SWAPLACE_ADDRESS || 0x0; /// @dev Will throw an error if any of the addresses were not set in the `.env` file. - if (!ERC20_ADDRESS || !ERC721_ADDRESS || !SWAPLACE_ADDRESS) { + if ( + !ERC20_ADDRESS || + !ERC721_ADDRESS || + !SWAPLACE_ADDRESS || + !ERC1155_ADDRESS + ) { throw new Error( - "Invalid ERC20, ERC721 or Swaplace address, please check if the addresse in the `.env` file is set up correctly.", + "Invalid ERC20, ERC721, ERC1155 or Swaplace address, please check if the addresses in the `.env` file are set up correctly.", ); } @@ -65,21 +76,26 @@ export async function main() { ); } - /// @dev Fill the Swap struct + /// @dev Fill the Swap struct and config const owner = signers[0].address; const allowed = ethers.constants.AddressZero; const expiry = (await blocktimestamp()) * 2; + const recipient = 0; + const value = 0; + + /// @dev Encode ERC1155 asset + const amountAndId = await encodeAsset(BigInt(tokenId), BigInt(amount)); /// @dev Build the biding assets - const bidingAddr = [ERC20_ADDRESS]; - const bidingAmountOrId = [amount]; + const bidingAddr = [ERC20_ADDRESS, ERC1155_ADDRESS]; + const bidingAmountOrId = [BigInt(amount), amountAndId]; /// @dev Build the asking assets const askingAddr = [ERC721_ADDRESS]; - const askingAmountOrId = [tokenId]; + const askingAmountOrId = [BigInt(tokenId)]; /// @dev Pack the config together - const config = (BigInt(allowed) << BigInt(96)) | BigInt(expiry); + const config = await encodeConfig(allowed, expiry, recipient, value); /// @dev Compose the above swap into the Swap Struct const swap: Swap = await composeSwap( diff --git a/scripts/deployMock.ts b/scripts/deployMock.ts index f8266d3..660c10e 100644 --- a/scripts/deployMock.ts +++ b/scripts/deployMock.ts @@ -11,6 +11,7 @@ async function main() { /// @dev The returned contract instance that will be deployed via the deploy function in utils. let MockERC20: Contract; let MockERC721: Contract; + let MockERC1155: Contract; /// @dev will throw an error if any of the accounts was not set up correctly. try { @@ -27,6 +28,7 @@ async function main() { // We are deploying both contracts to test the user flux with the entire functionality. MockERC20 = await deploy("MockERC20", signers[0]); MockERC721 = await deploy("MockERC721", signers[0]); + MockERC1155 = await deploy("MockERC1155", signers[0]); // @dev Log Contract address and the Tx hash which can be searched on Etherscan (or any other block explorer). console.log( @@ -43,13 +45,22 @@ async function main() { MockERC721.deployTransaction.hash, ); + console.log( + "\nContract %s \nDeployed to %s \nAt Tx %s\n", + "MockERC1155", + MockERC1155.address, + MockERC1155.deployTransaction.hash, + ); + /// @dev Store the contract addresses in the .env file. await storeEnv(MockERC20.address, "ERC20_ADDRESS", true); await storeEnv(MockERC721.address, "ERC721_ADDRESS", true); + await storeEnv(MockERC1155.address, "ERC1155_ADDRESS", true); /// @dev Awaits for the transaction to be mined. await MockERC20.deployed(); await MockERC721.deployed(); + await MockERC1155.deployed(); } main().catch((error) => { diff --git a/scripts/mint.ts b/scripts/mint.ts index f96d286..64ee9a0 100644 --- a/scripts/mint.ts +++ b/scripts/mint.ts @@ -11,10 +11,11 @@ async function main() { /// @dev This is the list of mock deployments addresses that were stored in the `.env` file. const ERC20_ADDRESS = process.env.ERC20_ADDRESS || 0x0; const ERC721_ADDRESS = process.env.ERC721_ADDRESS || 0x0; + const ERC1155_ADDRESS = process.env.ERC1155_ADDRESS || 0x0; /// @dev Will throw an error if any of the addresses were not set in the `.env` file. - if (!ERC20_ADDRESS || !ERC721_ADDRESS) { + if (!ERC20_ADDRESS || !ERC721_ADDRESS || !ERC1155_ADDRESS) { throw new Error( - "Invalid ERC20 or ERC721 address, please check if the addresse in the `.env` file is set up correctly.", + "Invalid ERC20 or ERC721 or ERC1155 address, please check if the addresses in the `.env` file are set up correctly.", ); } @@ -25,6 +26,7 @@ async function main() { /// @dev The returned contract instance that will be deployed via the deploy function in utils. let MockERC20: Contract; let MockERC721: Contract; + let MockERC1155: Contract; /// @dev will throw an error if any of the accounts was not set up correctly. try { @@ -49,6 +51,11 @@ async function main() { ERC721_ADDRESS, signers[0], ); + MockERC1155 = await ethers.getContractAt( + "MockERC1155", + ERC1155_ADDRESS, + signers[0], + ); } catch (error) { throw new Error( `Error deploying one of the Mock Contracts. @@ -69,6 +76,7 @@ async function main() { /// @dev Responses from the minting transactions. let txErc20; let txErc721; + let txErc1155; /// @dev Minting function will throw an error if the minting fails. /// We are minting for the first signer of `hardhat.config.ts` 1000 @@ -77,6 +85,7 @@ async function main() { try { txErc20 = await MockERC20.mint(signers[0].address, amount); txErc721 = await MockERC721.mint(signers[0].address, tokenId); + txErc1155 = await MockERC1155.mint(signers[0].address, tokenId, amount); } catch (error) { throw new Error( `Error while minting tokens. Make sure that the minting function is @@ -93,12 +102,19 @@ async function main() { tokenId, txErc721.hash, ); + console.log( + "\nERC1155 Minted %s tokens with ID #%s \nAt Tx %s", + amount, + tokenId, + txErc1155.hash, + ); await storeEnv(tokenId, "TOKEN_ID", false); /// @dev Awaits for the transaction to be mined. await txErc20.wait(); await txErc721.wait(); + await txErc1155.wait(); } main().catch((error) => { diff --git a/test/TestMockContracts.test.ts b/test/TestMockContracts.test.ts index 545b125..b7be7eb 100644 --- a/test/TestMockContracts.test.ts +++ b/test/TestMockContracts.test.ts @@ -8,6 +8,7 @@ describe("Swaplace", async function () { // The deployed contracts let MockERC20: Contract; let MockERC721: Contract; + let MockERC1155: Contract; // The signers of the test let deployer: SignerWithAddress; @@ -18,27 +19,32 @@ describe("Swaplace", async function () { [deployer, owner, acceptee] = await ethers.getSigners(); MockERC20 = await deploy("MockERC20", deployer); MockERC721 = await deploy("MockERC721", deployer); + MockERC1155 = await deploy("MockERC1155", deployer); }); it("Should test the {mint} function", async function () { - // Testing the mint of ERC20 await MockERC20.mint(owner.address, 1000); expect(await MockERC20.balanceOf(owner.address)).to.be.equals(1000); - // Testing the mint of ERC721 await MockERC721.mint(owner.address, 1); expect(await MockERC721.balanceOf(owner.address)).to.be.equals(1); + + await MockERC1155.mint(owner.address, 1, 5); }); it("Should test the {approve} function", async function () { - // Testing the approval of ERC20 await MockERC20.connect(owner).approve(acceptee.address, 1000); expect( await MockERC20.allowance(owner.address, acceptee.address), ).to.be.equals("1000"); - // Testing the approval of ERC721 + await MockERC721.connect(owner).approve(acceptee.address, 1); expect(await MockERC721.getApproved(1)).to.be.equals(acceptee.address); + + await MockERC1155.connect(owner).setApprovalForAll(acceptee.address, true); + expect( + await MockERC1155.isApprovedForAll(owner.address, acceptee.address), + ).to.be.equals(true); }); it("Should test the {transferFrom} function", async function () { @@ -54,5 +60,14 @@ describe("Swaplace", async function () { ); expect(await MockERC721.balanceOf(owner.address)).to.be.equals(0); expect(await MockERC721.balanceOf(acceptee.address)).to.be.equals(1); + // Testing the transfer of ERC1155 + await MockERC1155.connect(owner).safeTransferFrom( + owner.address, + acceptee.address, + 1, + 5, + "0x", + ); + expect(await MockERC1155.balanceOf(acceptee.address, 1)).to.be.equals(5); }); }); diff --git a/test/TestSwapFactory.test.ts b/test/TestSwapFactory.test.ts index bb1079d..12e88f0 100644 --- a/test/TestSwapFactory.test.ts +++ b/test/TestSwapFactory.test.ts @@ -1,8 +1,16 @@ import { expect } from "chai"; -import { Contract } from "ethers"; +import { BigNumber, Contract } from "ethers"; import { ethers } from "hardhat"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { Asset, makeAsset, makeSwap, composeSwap } from "./utils/SwapFactory"; +import { + Asset, + makeAsset, + makeSwap, + composeSwap, + encodeConfig, + decodeConfig, + Swap, +} from "./utils/SwapFactory"; import { blocktimestamp, deploy } from "./utils/utils"; describe("Swaplace Factory", async function () { @@ -10,6 +18,7 @@ describe("Swaplace Factory", async function () { let Swaplace: Contract; let MockERC20: Contract; let MockERC721: Contract; + let MockERC1155: Contract; // The signers of the test let deployer: SignerWithAddress; @@ -23,16 +32,17 @@ describe("Swaplace Factory", async function () { Swaplace = await deploy("Swaplace", deployer); MockERC20 = await deploy("MockERC20", deployer); MockERC721 = await deploy("MockERC721", deployer); + MockERC1155 = await deploy("MockERC1155", deployer); }); it("Should be able to {makeAsset} for ERC20 and ERC721", async function () { var asset: Asset = await makeAsset(MockERC20.address, 1000); expect(asset.addr).to.be.equals(MockERC20.address); - expect(asset.amountOrId).to.be.equals("1000"); + expect(asset.amountOrId).to.be.equals(1000); var asset: Asset = await makeAsset(MockERC721.address, 1); expect(asset.addr).to.be.equals(MockERC721.address); - expect(asset.amountOrId).to.be.equals("1"); + expect(asset.amountOrId).to.be.equals(1); }); it("Should be able to {makeAsset} in the off-chain matching on-chain", async function () { @@ -40,16 +50,48 @@ describe("Swaplace Factory", async function () { var asset: Asset = await makeAsset(MockERC20.address, 1000); expect(asset.addr).to.be.equals(MockERC20.address); - expect(asset.amountOrId).to.be.equals("1000"); + expect(asset.amountOrId).to.be.equals(1000); + }); + + it("Should be able to encode and decode config using off-chain", async function () { + const currentTimestamp = (await blocktimestamp()) + 2000000; + const configOnChain = await Swaplace.encodeConfig( + Swaplace.address, + currentTimestamp, + 0, + 0, + ); + const configOffChain = await encodeConfig( + Swaplace.address, + currentTimestamp, + 0, + 0, + ); + expect(configOnChain).to.be.equals(configOffChain); + + const [allowed, expiry, recipient, value] = await Swaplace.decodeConfig( + configOnChain, + ); + const decodedConfig = await decodeConfig(configOffChain); + + expect(BigInt(allowed)).to.be.equals(decodedConfig.allowed); + expect(expiry).to.be.equals(decodedConfig.expiry); + expect(recipient).to.be.equals(decodedConfig.recipient); + expect(value).to.be.equals(decodedConfig.value); }); it("Should be able to {makeSwap} with ERC20 and ERC721", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; const ERC20Asset: Asset = await makeAsset(MockERC20.address, 1000); const ERC721Asset: Asset = await makeAsset(MockERC721.address, 1); - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap = await makeSwap( owner.address, @@ -58,7 +100,7 @@ describe("Swaplace Factory", async function () { [ERC721Asset], ); - const [allowed, expiry] = await Swaplace.parseData(swap.config); + const [allowed, expiry, ,] = await Swaplace.decodeConfig(swap.config); expect(swap.owner).to.be.equals(owner.address); expect(expiry).to.be.equals(currentTimestamp); @@ -68,12 +110,17 @@ describe("Swaplace Factory", async function () { }); it("Should be able to {makeSwap} in the off-chain matching on-chain", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; const ERC20Asset: Asset = await makeAsset(MockERC20.address, 1000); const ERC721Asset: Asset = await makeAsset(MockERC721.address, 1); - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap = await makeSwap( owner.address, @@ -86,12 +133,14 @@ describe("Swaplace Factory", async function () { owner.address, zeroAddress, currentTimestamp, + 0, + 0, [ERC20Asset], [ERC721Asset], ); - const [allowed, expiry] = await Swaplace.parseData(swap.config); - const [onChainAllowed, onChainExpiry] = await Swaplace.parseData( + const [allowed, expiry, ,] = await Swaplace.decodeConfig(swap.config); + const [onChainAllowed, onChainExpiry, ,] = await Swaplace.decodeConfig( onChainSwap.config, ); @@ -110,13 +159,73 @@ describe("Swaplace Factory", async function () { ); }); + it("Should be able to {makeSwap} with native ethers value", async function () { + const currentTimestamp = (await blocktimestamp()) + 2000000; + + const ERC20Asset: Asset = await makeAsset(MockERC20.address, 1000); + const ERC721Asset: Asset = await makeAsset(MockERC721.address, 1); + + const valueToSend: BigNumber = ethers.utils.parseEther("0.2"); + + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 1, + valueToSend.div(1e12), + ); + + const swap = await makeSwap( + owner.address, + config, + [ERC20Asset], + [ERC721Asset], + ); + + const onChainSwap = await Swaplace.makeSwap( + owner.address, + zeroAddress, + currentTimestamp, + 1, + valueToSend.div(1e12), + [ERC20Asset], + [ERC721Asset], + ); + + const [allowed, expiry, recipient, value] = await Swaplace.decodeConfig( + swap.config, + ); + const [onChainAllowed, onChainExpiry, onChainRecipient, onChainValue] = + await Swaplace.decodeConfig(onChainSwap.config); + + expect(swap.owner).to.be.equals(onChainSwap.owner); + expect(expiry).to.be.equals(onChainExpiry); + expect(allowed).to.be.equals(onChainAllowed); + expect(recipient).to.be.equals(onChainRecipient); + expect(value).to.be.equals(onChainValue); + + expect(swap.biding[0].addr).to.be.equals(onChainSwap.biding[0].addr); + expect(swap.biding[0].amountOrId).to.be.equals( + onChainSwap.biding[0].amountOrId, + ); + + expect(swap.asking[0].addr).to.be.equals(onChainSwap.asking[0].addr); + expect(swap.asking[0].amountOrId).to.be.equals( + onChainSwap.asking[0].amountOrId, + ); + }); + it("Should be able to {makeSwap} with multiple assets", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; const ERC20Asset = await makeAsset(MockERC20.address, 1000); const ERC721Asset = await makeAsset(MockERC721.address, 1); - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap = await makeSwap( owner.address, @@ -125,7 +234,7 @@ describe("Swaplace Factory", async function () { [ERC20Asset, ERC721Asset], ); - const [, expiry] = await Swaplace.parseData(swap.config); + const [, expiry, ,] = await Swaplace.decodeConfig(swap.config); expect(swap.owner).to.be.equals(owner.address); expect(expiry).to.be.equals(expiry); @@ -135,8 +244,150 @@ describe("Swaplace Factory", async function () { expect(swap.asking[1]).to.be.equals(ERC721Asset); }); + it("Should be able to {makeSwap} with ERC1155 tokens", async function () { + const bidingAddr = [MockERC1155.address]; + const tokenId = 1; + const amount = 3; + const amountAndId = await Swaplace.encodeAsset(tokenId, amount); + const bidingAmountOrId = [amountAndId]; + + const askingAddr = [MockERC721.address]; + const askingAmountOrId = [50]; + + const ERC1155Asset: Asset = await makeAsset( + bidingAddr[0], + bidingAmountOrId[0], + ); + const ERC721Asset: Asset = await makeAsset( + askingAddr[0], + askingAmountOrId[0], + ); + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); + + const swap = await makeSwap( + owner.address, + config, + [ERC1155Asset], + [ERC721Asset], + ); + + const onChainSwap = await Swaplace.makeSwap( + owner.address, + zeroAddress, + currentTimestamp, + 0, + 0, + [ERC1155Asset], + [ERC721Asset], + ); + + const [allowed, expiry, recipient, value] = await Swaplace.decodeConfig( + swap.config, + ); + + const [onChainAllowed, onChainExpiry, onChainRecipient, onChainValue] = + await Swaplace.decodeConfig(onChainSwap.config); + + expect(swap.owner).to.be.equals(onChainSwap.owner); + expect(expiry).to.be.equals(onChainExpiry); + expect(allowed).to.be.equals(onChainAllowed); + expect(recipient).to.be.equals(onChainRecipient); + expect(value).to.be.equals(onChainValue); + expect(swap.biding[0].addr).to.be.equals(onChainSwap.biding[0].addr); + expect(swap.biding[0].amountOrId).to.be.equals( + onChainSwap.biding[0].amountOrId, + ); + + expect(swap.asking[0].addr).to.be.equals(onChainSwap.asking[0].addr); + expect(swap.asking[0].amountOrId).to.be.equals( + onChainSwap.asking[0].amountOrId, + ); + }); + + it("Should be able to {composeSwap} using ERC1155", async function () { + const bidingAddr = [MockERC1155.address]; + const tokenId = 1; + const amount = 3; + const amountAndId = await Swaplace.encodeAsset(tokenId, amount); + const bidingAmountOrId = [amountAndId]; + + const askingAddr = [MockERC721.address]; + const askingAmountOrId = [50]; + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + const [allowed, expiry, recipient, value] = await Swaplace.decodeConfig( + swap.config, + ); + + expect(swap.owner).to.be.equals(owner.address); + expect(allowed).to.be.equals(zeroAddress); + expect(expiry).to.be.equals(expiry); + expect(recipient).to.be.equals(0); + expect(value).to.be.equals(0); + expect(swap.biding[0].addr).to.be.equals(bidingAddr[0]); + expect(swap.biding[0].amountOrId).to.be.equals(bidingAmountOrId[0]); + + expect(swap.asking[0].addr).to.be.equals(askingAddr[0]); + expect(swap.asking[0].amountOrId).to.be.equals(askingAmountOrId[0]); + }); + it("Should be able to {composeSwap} using both ERC20, ERC721", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; + + const bidingAddr = [MockERC20.address, MockERC721.address]; + const bidingAmountOrId = [1000, 1]; + + const askingAddr = [MockERC721.address]; + const askingAmountOrId = [2]; + + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); + + const swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + const [allowed, expiry, ,] = await Swaplace.decodeConfig(swap.config); + + expect(swap.owner).to.be.equals(owner.address); + expect(allowed).to.be.equals(zeroAddress); + expect(expiry).to.be.equals(expiry); + }); + + it("Should be able to {composeSwap} using native ethers value", async function () { + const currentTimestamp = (await blocktimestamp()) + 2000000; const bidingAddr = [MockERC20.address, MockERC721.address]; const bidingAmountOrId = [1000, 1]; @@ -144,7 +395,14 @@ describe("Swaplace Factory", async function () { const askingAddr = [MockERC721.address]; const askingAmountOrId = [2]; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const valueToSend: BigNumber = ethers.utils.parseEther("1"); + + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + valueToSend.div(1e12), + ); const swap = await composeSwap( owner.address, @@ -155,11 +413,15 @@ describe("Swaplace Factory", async function () { askingAmountOrId, ); - const [allowed, expiry] = await Swaplace.parseData(swap.config); + const [allowed, expiry, recipient, value] = await Swaplace.decodeConfig( + swap.config, + ); expect(swap.owner).to.be.equals(owner.address); expect(allowed).to.be.equals(zeroAddress); expect(expiry).to.be.equals(expiry); + expect(recipient).to.be.equals(0); + expect(value).to.be.equals(valueToSend.div(1e12)); }); it("Should revert using {composeSwap} without minimum expiry", async function () { @@ -172,7 +434,7 @@ describe("Swaplace Factory", async function () { const askingAmountOrId = [2]; try { - const config = await Swaplace.packData(zeroAddress, expiry); + const config = await Swaplace.encodeConfig(zeroAddress, expiry, 0, 0); await composeSwap( owner.address, config, @@ -187,7 +449,7 @@ describe("Swaplace Factory", async function () { }); it("Should revert using {composeSwap} with owner as address zero", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; const bidingAddr = [MockERC20.address]; const bidingAmountOrId = [1000]; @@ -196,7 +458,12 @@ describe("Swaplace Factory", async function () { const askingAmountOrId = [2]; try { - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); await composeSwap( zeroAddress, config, @@ -211,7 +478,7 @@ describe("Swaplace Factory", async function () { }); it("Should revert using {composeSwap} with empty assets", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; const bidingAddr = [MockERC20.address]; const bidingAmountOrId = [1000]; @@ -220,7 +487,12 @@ describe("Swaplace Factory", async function () { const askingAmountOrId: any[] = []; try { - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); await composeSwap( owner.address, config, @@ -235,7 +507,7 @@ describe("Swaplace Factory", async function () { }); it("Should revert using {composeSwap} with empty assets length", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + const currentTimestamp = (await blocktimestamp()) + 2000000; const bidingAddr = [MockERC20.address]; const bidingAmountOrId = [1000]; @@ -244,7 +516,12 @@ describe("Swaplace Factory", async function () { const askingAmountOrId = [1, 999, 777]; try { - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); await composeSwap( owner.address, config, @@ -258,12 +535,17 @@ describe("Swaplace Factory", async function () { } }); - it("Should ensure packData() and parseData() return the right values", async function () { - const currentTimestamp = (await blocktimestamp()) * 2; + it("Should ensure encodeConfig() and decodeConfig() return the right values", async function () { + const currentTimestamp = (await blocktimestamp()) + 2000000; - const config = await Swaplace.packData(acceptee.address, currentTimestamp); + const config = await Swaplace.encodeConfig( + acceptee.address, + currentTimestamp, + 0, + 0, + ); - const [allowed, expiry] = await Swaplace.parseData(config); + const [allowed, expiry, ,] = await Swaplace.decodeConfig(config); expect(allowed).to.be.equals(acceptee.address); expect(expiry).to.be.equals(currentTimestamp); diff --git a/test/TestSwaplace.test.ts b/test/TestSwaplace.test.ts index 76bcdc0..13be114 100644 --- a/test/TestSwaplace.test.ts +++ b/test/TestSwaplace.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { Contract } from "ethers"; +import { BigNumber, Contract } from "ethers"; import { ethers, network } from "hardhat"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { Asset, Swap, composeSwap } from "./utils/SwapFactory"; @@ -10,6 +10,7 @@ describe("Swaplace", async function () { let Swaplace: Contract; let MockERC20: Contract; let MockERC721: Contract; + let MockERC1155: Contract; // The signers of the test let deployer: SignerWithAddress; @@ -32,8 +33,14 @@ describe("Swaplace", async function () { const askingAddr = [MockERC20.address]; const askingAmountOrId = [50]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -52,10 +59,260 @@ describe("Swaplace", async function () { Swaplace = await deploy("Swaplace", deployer); MockERC20 = await deploy("MockERC20", deployer); MockERC721 = await deploy("MockERC721", deployer); + MockERC1155 = await deploy("MockERC1155", deployer); }); describe("Creating Swaps", () => { context("Creating different types of Swaps", () => { + it("Should be able to create a 1-1 swap with ERC1155", async function () { + const bidingAddr = [MockERC1155.address]; + const tokenId = 1; + const amount = 3; + const amountAndId = await Swaplace.encodeAsset(tokenId, amount); + const bidingAmountOrId = [amountAndId]; + + const askingAddr = [MockERC721.address]; + const askingAmountOrId = [50]; + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + allowed.address, + currentTimestamp, + 0, + 0, + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await MockERC1155.mint(owner.address, tokenId, amount); + await MockERC1155.connect(owner).setApprovalForAll( + Swaplace.address, + true, + ); + await MockERC721.mint(allowed.address, askingAmountOrId[0]); + await MockERC721.connect(allowed).approve( + Swaplace.address, + askingAmountOrId[0], + ); + + const nextSwapId = Number(await Swaplace.totalSwaps()) + 1; + + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs(nextSwapId, owner.address, allowed.address); + + await expect( + await Swaplace.connect(allowed).acceptSwap( + nextSwapId, + receiver.address, + ), + ) + .to.emit(Swaplace, "SwapAccepted") + .withArgs(nextSwapId, owner.address, allowed.address); + + expect( + await MockERC1155.balanceOf(owner.address, tokenId), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId), + ).to.be.equals(amount); + expect(await MockERC721.ownerOf(askingAmountOrId[0])).to.be.equals( + owner.address, + ); + expect(await MockERC721.balanceOf(allowed.address)).to.be.equals(0); + }); + + it("Should be able to create a 1-N swap with ERC1155", async function () { + const bidingAddr = [ + MockERC1155.address, + MockERC1155.address, + MockERC1155.address, + ]; + const tokenId1 = 69; + const amount1 = 3; + const tokenId2 = 2; + const amount2 = 6; + const tokenId3 = 3; + const amount3 = 9; + const amountAndId1 = await Swaplace.encodeAsset(tokenId1, amount1); + const amountAndId2 = await Swaplace.encodeAsset(tokenId2, amount2); + const amountAndId3 = await Swaplace.encodeAsset(tokenId3, amount3); + const bidingAmountOrId = [amountAndId1, amountAndId2, amountAndId3]; + + const askingAddr = [MockERC721.address]; + const askingAmountOrId = [69]; + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + allowed.address, + currentTimestamp, + 0, + 0, + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await MockERC1155.mint(owner.address, tokenId1, amount1); + await MockERC1155.mint(owner.address, tokenId2, amount2); + await MockERC1155.mint(owner.address, tokenId3, amount3); + await MockERC1155.connect(owner).setApprovalForAll( + Swaplace.address, + true, + ); + await MockERC721.mint(allowed.address, askingAmountOrId[0]); + await MockERC721.connect(allowed).approve( + Swaplace.address, + askingAmountOrId[0], + ); + + const nextSwapId = Number(await Swaplace.totalSwaps()) + 1; + + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs(nextSwapId, owner.address, allowed.address); + + await expect( + await Swaplace.connect(allowed).acceptSwap( + nextSwapId, + receiver.address, + ), + ) + .to.emit(Swaplace, "SwapAccepted") + .withArgs(nextSwapId, owner.address, allowed.address); + + expect( + await MockERC1155.balanceOf(owner.address, tokenId1), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(owner.address, tokenId2), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(owner.address, tokenId3), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId1), + ).to.be.equals(amount1); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId2), + ).to.be.equals(amount2); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId3), + ).to.be.equals(amount3); + expect(await MockERC721.ownerOf(askingAmountOrId[0])).to.be.equals( + owner.address, + ); + expect(await MockERC721.balanceOf(allowed.address)).to.be.equals(0); + }); + + it("Should be able to create a N-N swap with ERC1155", async function () { + const bidingAddr = [ + MockERC1155.address, + MockERC1155.address, + MockERC1155.address, + ]; + const tokenId1 = 4; + const amount1 = 69; + const tokenId2 = 5; + const amount2 = 69; + const tokenId3 = 6; + const amount3 = 69; + const amountAndId1 = await Swaplace.encodeAsset(tokenId1, amount1); + const amountAndId2 = await Swaplace.encodeAsset(tokenId2, amount2); + const amountAndId3 = await Swaplace.encodeAsset(tokenId3, amount3); + const bidingAmountOrId = [amountAndId1, amountAndId2, amountAndId3]; + + const askingAddr = [ + MockERC721.address, + MockERC721.address, + MockERC721.address, + ]; + const askingAmountOrId = [59, 79, 89]; + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + allowed.address, + currentTimestamp, + 0, + 0, + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await MockERC1155.mint(owner.address, tokenId1, amount1); + await MockERC1155.mint(owner.address, tokenId2, amount2); + await MockERC1155.mint(owner.address, tokenId3, amount3); + await MockERC1155.connect(owner).setApprovalForAll( + Swaplace.address, + true, + ); + await MockERC721.mint(allowed.address, askingAmountOrId[0]); + await MockERC721.mint(allowed.address, askingAmountOrId[1]); + await MockERC721.mint(allowed.address, askingAmountOrId[2]); + await MockERC721.connect(allowed).setApprovalForAll( + Swaplace.address, + true, + ); + + const nextSwapId = Number(await Swaplace.totalSwaps()) + 1; + + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs(nextSwapId, owner.address, allowed.address); + + await expect( + await Swaplace.connect(allowed).acceptSwap( + nextSwapId, + receiver.address, + ), + ) + .to.emit(Swaplace, "SwapAccepted") + .withArgs(nextSwapId, owner.address, allowed.address); + + expect( + await MockERC1155.balanceOf(owner.address, tokenId1), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(owner.address, tokenId2), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(owner.address, tokenId3), + ).to.be.equals(0); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId1), + ).to.be.equals(amount1); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId2), + ).to.be.equals(amount2); + expect( + await MockERC1155.balanceOf(receiver.address, tokenId3), + ).to.be.equals(amount3); + expect(await MockERC721.ownerOf(askingAmountOrId[0])).to.be.equals( + owner.address, + ); + expect(await MockERC721.balanceOf(allowed.address)).to.be.equals(0); + }); + it("Should be able to create a 1-1 swap with ERC20", async function () { const bidingAddr = [MockERC20.address]; const bidingAmountOrId = [50]; @@ -63,8 +320,13 @@ describe("Swaplace", async function () { const askingAddr = [MockERC20.address]; const askingAmountOrId = [50]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -91,8 +353,13 @@ describe("Swaplace", async function () { ]; const askingAmountOrId = [50, 100, 150]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -123,8 +390,13 @@ describe("Swaplace", async function () { ]; const askingAmountOrId = [50, 100, 150]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -147,8 +419,13 @@ describe("Swaplace", async function () { const askingAddr = [MockERC721.address]; const askingAmountOrId = [4]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -175,8 +452,13 @@ describe("Swaplace", async function () { ]; const askingAmountOrId = [4, 5, 6]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -207,8 +489,13 @@ describe("Swaplace", async function () { ]; const askingAmountOrId = [4, 5, 6]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); const swap: Swap = await composeSwap( owner.address, @@ -223,14 +510,119 @@ describe("Swaplace", async function () { .to.emit(Swaplace, "SwapCreated") .withArgs(await Swaplace.totalSwaps(), owner.address, zeroAddress); }); + + it("Should be able to {createSwap} with native ethers sent by the {owner}", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + await expect( + await Swaplace.connect(owner).createSwap(swap, { + value: valueToSend, + }), + ) + .to.emit(Swaplace, "SwapCreated") + .withArgs(await Swaplace.totalSwaps(), owner.address, zeroAddress); + }); }); context("Reverts when creating Swaps", () => { it("Should revert when {owner} is not {msg.sender}", async function () { const swap = await mockSwap(); - await expect(Swaplace.connect(allowed).createSwap(swap)) - .to.be.revertedWithCustomError(Swaplace, `InvalidAddress`) - .withArgs(allowed.address); + await expect( + Swaplace.connect(allowed).createSwap(swap), + ).to.be.revertedWithCustomError(Swaplace, `InvalidAddress`); + }); + + it("Should revert when the wrong amount of ethers are sent by the {owner}", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + await expect( + Swaplace.connect(owner).createSwap(swap, { value: 69 }), + ).to.be.revertedWithCustomError(Swaplace, `InvalidValue`); + }); + + it("Should revert when the {owner} sends ethers while being the {recipient}", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 1, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + await expect( + Swaplace.connect(owner).createSwap(swap, { value: valueToSend }), + ).to.be.revertedWithCustomError(Swaplace, `InvalidValue`); }); }); }); @@ -253,8 +645,13 @@ describe("Swaplace", async function () { const askingAddr = [MockERC20.address]; const askingAmountOrId = [1000]; - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + 0, + ); swap = await composeSwap( owner.address, @@ -328,9 +725,14 @@ describe("Swaplace", async function () { await MockERC721.connect(allowed).approve(Swaplace.address, 10); const swap = await mockSwap(); - const [, expiry] = await Swaplace.parseData(swap.config); + const [, expiry, ,] = await Swaplace.decodeConfig(swap.config); - swap.config = await Swaplace.packData(allowed.address, expiry); + swap.config = await Swaplace.encodeConfig( + allowed.address, + expiry, + 0, + 0, + ); await expect(await Swaplace.connect(owner).createSwap(swap)) .to.emit(Swaplace, "SwapCreated") @@ -353,6 +755,135 @@ describe("Swaplace", async function () { allowed.address, ); }); + + it("Should be able to {acceptSwap} with native ethers sent by the {owner}", async function () { + await MockERC20.mint(owner.address, 1000); + await MockERC721.mint(allowed.address, 10); + + await MockERC20.connect(owner).approve(Swaplace.address, 1000); + await MockERC721.connect(allowed).approve(Swaplace.address, 10); + + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + const expiry = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + allowed.address, + expiry, + 0, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + const balanceBefore: BigNumber = await receiver.getBalance(); + const expectedBalance: BigNumber = balanceBefore.add(valueToSend); + + await expect( + await Swaplace.connect(owner).createSwap(swap, { + value: valueToSend, + }), + ) + .to.emit(Swaplace, "SwapCreated") + .withArgs( + await Swaplace.totalSwaps(), + owner.address, + allowed.address, + ); + + await expect( + await Swaplace.connect(allowed).acceptSwap( + await Swaplace.totalSwaps(), + receiver.address, + ), + ) + .to.emit(Swaplace, "SwapAccepted") + .withArgs( + await Swaplace.totalSwaps(), + owner.address, + allowed.address, + ); + + const balanceAfter: BigNumber = await receiver.getBalance(); + await expect(balanceAfter).to.be.equals(expectedBalance); + }); + + it("Should be able to {acceptSwap} with native ethers sent by the {acceptee}", async function () { + await MockERC20.mint(owner.address, 1000); + await MockERC721.mint(allowed.address, 10); + + await MockERC20.connect(owner).approve(Swaplace.address, 1000); + await MockERC721.connect(allowed).approve(Swaplace.address, 10); + + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + const expiry = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + allowed.address, + expiry, + 1, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs( + await Swaplace.totalSwaps(), + owner.address, + allowed.address, + ); + + const balanceBefore: BigNumber = await owner.getBalance(); + const expectedBalance: BigNumber = balanceBefore.add(valueToSend); + + await expect( + Swaplace.connect(allowed).acceptSwap( + await Swaplace.totalSwaps(), + receiver.address, + { value: valueToSend }, + ), + ) + .to.emit(Swaplace, "SwapAccepted") + .withArgs( + await Swaplace.totalSwaps(), + owner.address, + allowed.address, + ); + + const balanceAfter: BigNumber = await owner.getBalance(); + await expect(balanceAfter).to.be.equals(expectedBalance); + }); }); context("Reverts when accepting Swaps", () => { @@ -377,26 +908,22 @@ describe("Swaplace", async function () { await Swaplace.totalSwaps(), receiver.address, ), - ) - .to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`) - .withArgs(0); + ).to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`); }); it("Should revert when {expiry} is smaller than {block.timestamp}", async function () { await Swaplace.connect(owner).createSwap(swap); - const [, expiry] = await Swaplace.parseData(swap.config); + const [, expiry, ,] = await Swaplace.decodeConfig(swap.config); - await network.provider.send("evm_increaseTime", [expiry * 2]); + await network.provider.send("evm_increaseTime", [2000000]); await expect( Swaplace.connect(owner).acceptSwap( await Swaplace.totalSwaps(), receiver.address, ), - ) - .to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`) - .withArgs(expiry); + ).to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`); }); it("Should revert when {allowance} is not provided", async function () { @@ -409,7 +936,7 @@ describe("Swaplace", async function () { await Swaplace.totalSwaps(), receiver.address, ), - ).to.be.revertedWith(`ERC721: caller is not token owner or approved`); + ).to.be.revertedWithCustomError(Swaplace, `InvalidCall`); }); it("Should revert when {acceptSwap} as not allowed to P2P Swap", async function () { @@ -421,8 +948,13 @@ describe("Swaplace", async function () { const swap = await mockSwap(); - const [, expiry] = await Swaplace.parseData(swap.config); - swap.config = await Swaplace.packData(deployer.address, expiry); + const [, expiry, ,] = await Swaplace.decodeConfig(swap.config); + swap.config = await Swaplace.encodeConfig( + deployer.address, + expiry, + 0, + 0, + ); await expect(await Swaplace.connect(owner).createSwap(swap)) .to.emit(Swaplace, "SwapCreated") @@ -437,21 +969,70 @@ describe("Swaplace", async function () { await Swaplace.totalSwaps(), receiver.address, ), - ) - .to.be.revertedWithCustomError(Swaplace, "InvalidAddress") - .withArgs(allowed.address); + ).to.be.revertedWithCustomError(Swaplace, "InvalidAddress"); + }); + + it("Should revert when wrong amount of ethers are sent by the {acceptee}", async function () { + await MockERC20.mint(owner.address, 1000); + await MockERC721.mint(allowed.address, 10); + + await MockERC20.connect(owner).approve(Swaplace.address, 1000); + await MockERC721.connect(allowed).approve(Swaplace.address, 10); + + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + const expiry = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + allowed.address, + expiry, + 1, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs( + await Swaplace.totalSwaps(), + owner.address, + allowed.address, + ); + + await expect( + Swaplace.connect(allowed).acceptSwap( + await Swaplace.totalSwaps(), + receiver.address, + { value: 69 }, + ), + ).to.be.revertedWithCustomError(Swaplace, "InvalidValue"); }); }); }); describe("Canceling Swaps", () => { - context("Canceling Swaps", () => { - var swap: Swap; - before(async () => { - swap = await mockSwap(); - await Swaplace.connect(owner).createSwap(swap); - }); + var swap: Swap; + beforeEach(async () => { + swap = await mockSwap(); + await Swaplace.connect(owner).createSwap(swap); + }); + context("Canceling Swaps", () => { it("Should be able to {cancelSwap} a Swap", async function () { const lastSwap = await Swaplace.totalSwaps(); await expect(await Swaplace.connect(owner).cancelSwap(lastSwap)) @@ -461,68 +1042,228 @@ describe("Swaplace", async function () { it("Should not be able to {acceptSwap} a canceled a Swap", async function () { const lastSwap = await Swaplace.totalSwaps(); + await Swaplace.connect(owner).acceptSwap(lastSwap, receiver.address), + await expect( + Swaplace.connect(owner).acceptSwap(lastSwap, receiver.address), + ).to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`); + }); + + it("Should be able to {cancelSwap} and return ethers to {owner}", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + await expect( - Swaplace.connect(owner).acceptSwap(lastSwap, receiver.address), + await Swaplace.connect(owner).createSwap(swap, { + value: valueToSend, + }), ) - .to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`) - .withArgs(0); - }); - }); + .to.emit(Swaplace, "SwapCreated") + .withArgs(await Swaplace.totalSwaps(), owner.address, zeroAddress); - context("Reverts when canceling Swaps", () => { - var swap: Swap; - before(async () => { - swap = await mockSwap(); - await Swaplace.connect(owner).createSwap(swap); + const balanceBefore = await owner.getBalance(); + + const lastSwap = await Swaplace.totalSwaps(); + const tx = await Swaplace.connect(owner).cancelSwap(lastSwap); + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed; + const gasPrice = receipt.effectiveGasPrice; + + const balanceAfter = await owner.getBalance(); + expect(balanceBefore.add(valueToSend)).to.be.equals( + balanceAfter.add(gasPrice.mul(gasUsed)), + ); }); - it("Should revert when {owner} is not {msg.sender}", async function () { + it("Should be able to {cancelSwap} and return ethers to {owner} even after expiration", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; + + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 0, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await expect( + await Swaplace.connect(owner).createSwap(swap, { + value: valueToSend, + }), + ) + .to.emit(Swaplace, "SwapCreated") + .withArgs(await Swaplace.totalSwaps(), owner.address, zeroAddress); + + await network.provider.send("evm_increaseTime", [1000000]); + + const balanceBefore = await owner.getBalance(); + const lastSwap = await Swaplace.totalSwaps(); - await expect(Swaplace.connect(allowed).cancelSwap(lastSwap)) - .to.be.revertedWithCustomError(Swaplace, `InvalidAddress`) - .withArgs(allowed.address); + const tx = await Swaplace.connect(owner).cancelSwap(lastSwap); + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed; + const gasPrice = receipt.effectiveGasPrice; + + const balanceAfter = await owner.getBalance(); + expect(balanceBefore.add(valueToSend)).to.be.equals( + balanceAfter.add(gasPrice.mul(gasUsed)), + ); }); - it("Should revert when {expiry} is smaller than {block.timestamp}", async function () { - const [, expiry] = await Swaplace.parseData(swap.config); + it("Should be able to {cancelSwap} before expiration if the recipient is the {owner}", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; + + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; - await network.provider.send("evm_increaseTime", [expiry * 2]); + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); + + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 1, + valueToSend.div(1e12), + ); + + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); + + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs(await Swaplace.totalSwaps(), owner.address, zeroAddress); const lastSwap = await Swaplace.totalSwaps(); - await expect(Swaplace.connect(owner).cancelSwap(lastSwap)) - .to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`) - .withArgs(expiry); + await expect(await Swaplace.connect(owner).cancelSwap(lastSwap)) + .to.emit(Swaplace, "SwapCanceled") + .withArgs(lastSwap, owner.address); + + await expect( + Swaplace.connect(owner).cancelSwap(lastSwap), + ).to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`); }); - }); - }); - describe("Fetching Swaps", () => { - var swap: Swap; - before(async () => { - MockERC20 = await deploy("MockERC20", deployer); - MockERC721 = await deploy("MockERC721", deployer); + it("Should not be able to {cancelSwap} after expiration if the recipient is the {owner}", async function () { + const bidingAddr = [MockERC20.address]; + const bidingAmountOrId = [50]; - await MockERC721.mint(owner.address, 1); - await MockERC20.mint(allowed.address, 1000); + const askingAddr = [ + MockERC20.address, + MockERC20.address, + MockERC20.address, + ]; + const askingAmountOrId = [50, 100, 150]; - const bidingAddr = [MockERC721.address]; - const bidingAmountOrId = [1]; + const valueToSend: BigNumber = ethers.utils.parseEther("0.5"); - const askingAddr = [MockERC20.address]; - const askingAmountOrId = [1000]; + const currentTimestamp = (await blocktimestamp()) + 1000000; + const config = await Swaplace.encodeConfig( + zeroAddress, + currentTimestamp, + 1, + valueToSend.div(1e12), + ); - const currentTimestamp = (await blocktimestamp()) * 2; - const config = await Swaplace.packData(zeroAddress, currentTimestamp); + const swap: Swap = await composeSwap( + owner.address, + config, + bidingAddr, + bidingAmountOrId, + askingAddr, + askingAmountOrId, + ); - swap = await composeSwap( - owner.address, - config, - bidingAddr, - bidingAmountOrId, - askingAddr, - askingAmountOrId, - ); + await expect(await Swaplace.connect(owner).createSwap(swap)) + .to.emit(Swaplace, "SwapCreated") + .withArgs(await Swaplace.totalSwaps(), owner.address, zeroAddress); + + await network.provider.send("evm_increaseTime", [1000000]); + + const lastSwap = await Swaplace.totalSwaps(); + await expect( + Swaplace.connect(owner).cancelSwap(lastSwap), + ).to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`); + }); + }); + context("Reverts when canceling Swaps", () => { + it("Should revert when {owner} is not {msg.sender}", async function () { + const lastSwap = await Swaplace.totalSwaps(); + await expect( + Swaplace.connect(allowed).cancelSwap(lastSwap), + ).to.be.revertedWithCustomError(Swaplace, `InvalidAddress`); + }); + + it("Should revert when {expiry} is smaller than {block.timestamp}", async function () { + await network.provider.send("evm_increaseTime", [2000000]); + + const lastSwap = await Swaplace.totalSwaps(); + await expect( + Swaplace.connect(owner).cancelSwap(lastSwap), + ).to.be.revertedWithCustomError(Swaplace, `InvalidExpiry`); + }); + }); + }); + + describe("Fetching Swaps", () => { + var swap: Swap; + beforeEach(async () => { + swap = await mockSwap(); await Swaplace.connect(owner).createSwap(swap); }); @@ -530,9 +1271,10 @@ describe("Swaplace", async function () { const lastSwap = await Swaplace.totalSwaps(); const fetchedSwap = await Swaplace.getSwap(lastSwap); + const [, expiry, ,] = await Swaplace.decodeConfig(swap.config); + expect(fetchedSwap.owner).not.to.be.equals(zeroAddress); - // swap.allowed can be the zero address and shoul not be trusted for validation - expect(fetchedSwap.expiry).not.to.be.equals(0); + expect(expiry).not.to.be.equals(0); expect(fetchedSwap.biding.length).to.be.greaterThan(0); expect(fetchedSwap.asking.length).to.be.greaterThan(0); }); @@ -542,7 +1284,7 @@ describe("Swaplace", async function () { const fetchedSwap = await Swaplace.getSwap(imaginarySwapId); // swap.allowed can be the zero address and shoul not be trusted for validation expect(fetchedSwap.owner).to.be.deep.equals(zeroAddress); - const [fetchedAllowed, fetchedExpiry] = await Swaplace.parseData( + const [fetchedAllowed, fetchedExpiry, ,] = await Swaplace.decodeConfig( fetchedSwap.config, ); expect(fetchedAllowed).to.be.deep.equals(zeroAddress); diff --git a/test/utils/SwapFactory.ts b/test/utils/SwapFactory.ts index 9879aa8..5f2c277 100644 --- a/test/utils/SwapFactory.ts +++ b/test/utils/SwapFactory.ts @@ -5,7 +5,7 @@ import { ethers } from "hardhat"; */ export interface Asset { addr: string; - amountOrId: bigint; + amountOrId: bigint | number; } /** @@ -13,17 +13,56 @@ export interface Asset { */ export interface Swap { owner: string; - config: number; + config: bigint; biding: Asset[]; asking: Asset[]; } +/** + * @dev See {ISwapFactory-encodeConfig}. + */ +export async function encodeConfig( + allowed: string, + expiry: bigint | number, + recipient: bigint | number, + value: bigint | number, +): Promise { + return ( + (BigInt(allowed) << BigInt(96)) | + (BigInt(expiry) << BigInt(64)) | + (BigInt(recipient) << BigInt(56)) | + BigInt(value) + ); +} + +/** + * @dev See {ISwapFactory-decodeConfig}. + */ +export async function decodeConfig(config: bigint): Promise<{ + allowed: string; + expiry: bigint | number; + recipient: bigint | number; + value: bigint | number; +}> { + return { + allowed: + config >> BigInt(96) == BigInt(0) + ? ethers.constants.AddressZero + : ethers.utils.getAddress( + `0x${(config >> BigInt(96)).toString(16).padStart(40, "0")}`, + ), + expiry: (config >> BigInt(64)) & ((BigInt(1) << BigInt(32)) - BigInt(1)), + recipient: (config >> BigInt(56)) & ((BigInt(1) << BigInt(8)) - BigInt(1)), + value: config & ((BigInt(1) << BigInt(56)) - BigInt(1)), + }; +} + /** * @dev See {ISwapFactory-makeAsset}. */ export async function makeAsset( addr: string, - amountOrId: number | bigint, + amountOrId: bigint | number, ): Promise { // validate if its an ethereum address if (!ethers.utils.isAddress(addr)) { @@ -43,18 +82,46 @@ export async function makeAsset( */ const asset: Asset = { addr: addr, - amountOrId: typeof amountOrId == "number" ? BigInt(amountOrId) : amountOrId, + amountOrId: amountOrId, }; return asset; } +/** + * @dev See {ISwapFactory-encodeAsset}. + */ +export async function encodeAsset( + tokenId: bigint | number, + tokenAmount: bigint | number, +): Promise { + // if the amount or ID is negative, it will throw an error + if (tokenId < 0 || tokenAmount < 0) { + throw new Error("tokenId or tokenAmount cannot be less than 0"); + } + + const uint16Max = 65535; + const uint120Max = BigInt(2) ** BigInt(120) - BigInt(1); + + if (tokenId > uint120Max || tokenAmount > uint120Max) { + throw new Error( + "Maxium bits exceeded for tokenId or tokenAmount. Max: 120 bits.", + ); + } + + return BigInt( + (BigInt(uint16Max) << BigInt(240)) | + (BigInt(tokenId) << BigInt(120)) | + BigInt(tokenAmount), + ); +} + /** * @dev See {ISwapFactory-makeSwap}. */ export async function makeSwap( - owner: any, - config: any, + owner: string, + config: bigint, biding: Asset[], asking: Asset[], ) { @@ -108,12 +175,12 @@ export async function makeSwap( * - `askingAddr` and `askingAmountOrId` must have the same length. */ export async function composeSwap( - owner: any, - config: any, - bidingAddr: any[], - bidingAmountOrId: any[], - askingAddr: any[], - askingAmountOrId: any[], + owner: string, + config: bigint, + bidingAddr: string[], + bidingAmountOrId: bigint[] | number[], + askingAddr: string[], + askingAmountOrId: bigint[] | number[], ) { // lenght of addresses and their respective amounts must be equal if ( @@ -124,12 +191,12 @@ export async function composeSwap( } // push new assets to the array of bids and asks - const biding: any[] = []; + const biding: Asset[] = []; bidingAddr.forEach(async (addr, index) => { biding.push(await makeAsset(addr, bidingAmountOrId[index])); }); - const asking: any[] = []; + const asking: Asset[] = []; askingAddr.forEach(async (addr, index) => { asking.push(await makeAsset(addr, askingAmountOrId[index])); }); @@ -141,4 +208,7 @@ module.exports = { makeAsset, makeSwap, composeSwap, + encodeConfig, + decodeConfig, + encodeAsset, }; diff --git a/test/utils/utils.ts b/test/utils/utils.ts index 486ad40..6c217bd 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -32,7 +32,8 @@ export async function deploy(contractName: any, signer: any) { throw new Error( `Error getting the Contract Factory for ${contractName}. Make sure the contract is compiled, the type-chain generated - and a valid Ethereum Address for signer set in hardhat.config.ts.`, + and a valid Ethereum Address for signer set in hardhat.config.ts. + ${error}`, ); } @@ -44,7 +45,8 @@ export async function deploy(contractName: any, signer: any) { `Error deploying the Contract ${contractName}. Make sure the network is correct, that you have a valid Ethereum Address for signer with enough funds for the transaction. The gas settings might - as well be lower than the amount required by the network at the moment.`, + as well be lower than the amount required by the network at the moment. + ${error}`, ); }