diff --git a/contracts/integrations/airswap/AirswapFeeConnector.sol b/contracts/integrations/airswap/AirswapFeeConnector.sol new file mode 100644 index 000000000..ef111f14d --- /dev/null +++ b/contracts/integrations/airswap/AirswapFeeConnector.sol @@ -0,0 +1,202 @@ +pragma solidity 0.5.17; + +import "../../openzeppelin/SafeMath.sol"; +import "../../openzeppelin/PausableOz.sol"; +import "../../openzeppelin/IERC20_.sol"; + +import "./IAirswapFeeConnector.sol"; +import "./IAirswapSwapERC20.sol"; + +contract AirswapFeeConnector is PausableOz, IAirswapFeeConnector { + using SafeMath for uint256; + + struct SwapRequest { + address sender; + address recipient; + uint256 nonce; + uint256 expiry; + address signerWallet; + address signerToken; + uint256 signerAmount; + address senderToken; + uint256 totalSenderAmount; + uint8 v; + bytes32 r; + bytes32 s; + } + + uint256 public constant POINTS = 1000; + + uint256 public inputFeeInPoints = 0; + uint256 public outputFeeInPoints = 0; + + address public feeVaultAddress = address(0); + address public swapERC20Address = address(0); + + event FeeVaultAddressChangedEvent(address indexed sender, address newAddress); + event SwapERC20AddressChangedEvent(address indexed sender, address newAddress); + event InputFeeChangedEvent(address indexed sender, uint256 feeInPoints); + event OutputFeeChangedEvent(address indexed sender, uint256 feeInPoints); + + event SwapEvent( + address indexed sender, + address indexed recipient, + address sendToken, + uint256 sendAmount, + uint256 inputFee, + address receiveToken, + uint256 receiveAmount, + uint256 outputFee + ); + + /// @notice Set the input fee in points, ie 25 means 2.5 percent. + /// The input fee is collected on the sent tokens before + /// the actual conversion. + /// @param _inputFeeInPoints The new fee in points + function setInputFee(uint256 _inputFeeInPoints) public onlyOwner { + inputFeeInPoints = _inputFeeInPoints; + emit InputFeeChangedEvent(msg.sender, inputFeeInPoints); + } + + /// @notice Set the output fee in points, ie 25 means 2.5 percent. + /// The output fee is collected after the conversion. + /// @param _outputFeeInPoints The new fee in points + function setOutputFee(uint256 _outputFeeInPoints) public onlyOwner { + outputFeeInPoints = _outputFeeInPoints; + emit OutputFeeChangedEvent(msg.sender, outputFeeInPoints); + } + + /// @notice Set the address to which fees are sent + /// @param _newAddress The new address + function setFeeVaultAddress(address _newAddress) public onlyOwner { + feeVaultAddress = _newAddress; + require(feeVaultAddress != address(0), "invalid vault"); + emit FeeVaultAddressChangedEvent(msg.sender, feeVaultAddress); + } + + /// @notice Set the address of the AirSwap contract + /// @param _newAddress The new address + function setSwapERC20Address(address _newAddress) public onlyOwner { + swapERC20Address = _newAddress; + require(swapERC20Address != address(0), "invalid swapper"); + emit SwapERC20AddressChangedEvent(msg.sender, swapERC20Address); + } + + function calculateInputFee(uint256 _sendAmount) public view returns (uint256) { + return _sendAmount.mul(inputFeeInPoints).div(POINTS); + } + + function calculateOutputFee(uint256 _receiveAmount) public view returns (uint256) { + return _receiveAmount.mul(outputFeeInPoints).div(POINTS); + } + + /// @notice Swap one token for another. + /// @param _sender Address which is sending the tokens + /// @param _recipient Address to send the resulting tokens after collecting the output fee + /// @param _nonce A one time nonce + /// @param _expiry Date at which the original proposal will expire + /// @param _signerWallet Address of the market maker wallet + /// @param _signerToken Address of the token to convert to + /// @param _signerAmount Amount of resulting token from the conversion + /// @param _totalSenderAmount The amount to be sent before the input fee is collected. + /// @param _v v part of the ECDSA signature + /// @param _r r part of the ECDSA signature + /// @param _s s part of the ECDSA signature + function swap( + address _sender, + address _recipient, + uint256 _nonce, + uint256 _expiry, + address _signerWallet, + address _signerToken, + uint256 _signerAmount, + address _senderToken, + uint256 _totalSenderAmount, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public { + require(feeVaultAddress != address(0), "invalid vault"); + require(swapERC20Address != address(0), "invalid swapper"); + + SwapRequest memory swapRequest = + SwapRequest( + _sender, + _recipient, + _nonce, + _expiry, + _signerWallet, + _signerToken, + _signerAmount, + _senderToken, + _totalSenderAmount, + _v, + _r, + _s + ); + + // first we move all the funds here + require( + IERC20_(swapRequest.senderToken).transferFrom( + swapRequest.sender, + address(this), + swapRequest.totalSenderAmount + ), + "transfer failed 1" + ); + + // then we collect the input fee + uint256 inputFee = calculateInputFee(swapRequest.totalSenderAmount); + require( + IERC20_(swapRequest.senderToken).transfer(feeVaultAddress, inputFee), + "transfer failed 2" + ); + + uint256 senderAmountAfterFee = swapRequest.totalSenderAmount.sub(inputFee); + + // now we do the swap + IAirswapSwapERC20(swapERC20Address).swap( + address(this), + swapRequest.nonce, + swapRequest.expiry, + swapRequest.signerWallet, + swapRequest.signerToken, + swapRequest.signerAmount, + swapRequest.senderToken, + senderAmountAfterFee, + swapRequest.v, + swapRequest.r, + swapRequest.s + ); + + // now we collect the output fee + uint256 outputFee = calculateOutputFee(swapRequest.signerAmount); + require( + IERC20_(swapRequest.signerToken).transfer(feeVaultAddress, outputFee), + "transfer failed 3" + ); + + uint256 receiveAmountAfterFee = swapRequest.signerAmount.sub(outputFee); + + // now we send the user her due + require( + IERC20_(swapRequest.signerToken).transfer( + swapRequest.recipient, + receiveAmountAfterFee + ), + "transfer failed 4" + ); + + // emit the event + emit SwapEvent( + swapRequest.sender, + swapRequest.recipient, + swapRequest.senderToken, + swapRequest.totalSenderAmount, + inputFee, + swapRequest.signerToken, + receiveAmountAfterFee, + outputFee + ); + } +} diff --git a/contracts/integrations/airswap/EnumerableMakerSet.sol b/contracts/integrations/airswap/EnumerableMakerSet.sol new file mode 100644 index 000000000..ebe0010fc --- /dev/null +++ b/contracts/integrations/airswap/EnumerableMakerSet.sol @@ -0,0 +1,143 @@ +pragma solidity ^0.5.0; + +import "./IAirswapFeeConnector.sol"; +/** + * @dev Based on Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * + * Include with `using EnumerableSet for EnumerableSet.AddressSet;`. + * + * _Available since v2.5.0._ + */ +library EnumerableMakerSet { + struct MakerSet { + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(address => uint256) index; + IAirswapFeeConnector.Maker[] values; + } + + /** + * @dev Add a value to a set. O(1). + * Returns false if the value was already in the set. + */ + function add(MakerSet storage set, IAirswapFeeConnector.Maker memory value) internal returns (bool) { + if (!contains(set, value.signer)) { + set.index[value.signer] = set.values.push(value); + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * Returns false if the value was not present in the set. + */ + function remove(MakerSet storage set, IAirswapFeeConnector.Maker memory value) internal returns (bool) { + if (contains(set, value.signer)) { + uint256 toDeleteIndex = set.index[value.signer] - 1; + uint256 lastIndex = set.values.length - 1; + + // If the element we're deleting is the last one, we can just remove it without doing a swap + if (lastIndex != toDeleteIndex) { + IAirswapFeeConnector.Maker memory lastValue = set.values[lastIndex]; + + // Move the last value to the index where the deleted value is + set.values[toDeleteIndex] = lastValue; + // Update the index for the moved value + set.index[lastValue.signer] = toDeleteIndex + 1; // All indexes are 1-based + } + + // Delete the index entry for the deleted value + delete set.index[value.signer]; + + // Delete the old entry for the moved value + set.values.pop(); + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(MakerSet storage set, address signer) internal view returns (bool) { + return set.index[signer] != 0; + } + + /** + * @dev Returns an array with all values in the set. O(N). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + + * WARNING: This function may run out of gas on large sets: use {length} and + * {get} instead in these cases. + */ + function enumerate(MakerSet storage set) internal view returns (IAirswapFeeConnector.Maker[] memory) { + IAirswapFeeConnector.Maker[] memory output = new IAirswapFeeConnector.Maker[](set.values.length); + for (uint256 i; i < set.values.length; i++) { + output[i] = set.values[i]; + } + return output; + } + + /** + * @dev Returns a chunk of array as recommended in enumerate() to avoid running of gas. + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + + * WARNING: This function may run out of gas on large sets: use {length} and + * {get} instead in these cases. + + * @param start start index of chunk + * @param count num of element to return; if count == 0 then returns all the elements from the @param start + */ + function enumerateChunk( + MakerSet storage set, + uint256 start, + uint256 count + ) internal view returns (IAirswapFeeConnector.Maker[] memory output) { + uint256 end = start + count; + require(end >= start, "addition overflow"); + end = (set.values.length < end || count == 0) ? set.values.length : end; + if (end == 0 || start >= end) { + return output; + } + + output = new IAirswapFeeConnector.Maker[](end - start); + for (uint256 i; i < end - start; i++) { + output[i] = set.values[i + start]; + } + return output; + } + + /** + * @dev Returns the number of elements on the set. O(1). + */ + function length(MakerSet storage set) internal view returns (uint256) { + return set.values.length; + } + + /** @dev Returns the element stored at position `index` in the set. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function get(MakerSet storage set, uint256 index) internal view returns (IAirswapFeeConnector.Maker memory) { + return set.values[index]; + } +} diff --git a/contracts/integrations/airswap/IAirswapFeeConnector.sol b/contracts/integrations/airswap/IAirswapFeeConnector.sol new file mode 100644 index 000000000..b5fb89a53 --- /dev/null +++ b/contracts/integrations/airswap/IAirswapFeeConnector.sol @@ -0,0 +1,102 @@ +pragma solidity 0.5.17; + +pragma experimental ABIEncoderV2; + +/** + * @title A proxy to the AirSwap ERC20 contract that collects fees before and after the conversion. + * @author Derek Mattr dharkmattr@gmail.com + */ +interface IAirswapFeeConnector { + + /// Maker + struct Maker { + /// @dev name of the market maker + string makerName; + /// @dev address of the signer who signs the order + address signer; + /// @dev address of the maker to send the fees + address feeReceiver; + } + + /// @notice Return array of makers registered in the contract + /// @return Array of Maker objects + function getMakers() external view returns (Maker[] memory); + + /// @notice Returns the rfq amount after subtracting the fees + /// @param amount Amount provided by the user for trade + /// @param splitAmmFees This param indicates if the fee should be divided by half and sent to 2 pools + /// @return Quote amount for trade after deducting the fees + function getRfqAmount(uint256 amount, bool splitAmmFees) external view returns (uint256); + + /// @notice Checks if the token address taken from the RFQ order is valid + /// @param converter Address of the converter used to validate token + /// @param token Address of the token to validate + /// @param isSourceToken To specify if the token is source or destination + /// @return isTokenSupported Checks if the token is supported by Sovryn protocol + /// @return isConverterValid Checks if the provided converter address is valid + /// @return isTokenValid Checks if the token provided is valid + function isValidConverterToken(address converter, address token, bool isSourceToken) + external + view + returns (bool isTokenSupported, bool isConverterValid, bool isTokenValid); + + /// @notice Returns the total fee percentage in bps + /// @return Sum of all fee percentages in bps + function getTotalBps() external view returns(uint256); + + /// @notice This function can be used to add new maker details + /// @param maker Maker struct providing the details of maker to add + function addMaker(Maker calldata maker) external; + + /// @notice Removes the maker from the list + /// @param signer Address of the signer used by maker + function removeMaker(address signer) external; + + /// @notice Update the signer address of the maker + /// @param fromAddress Current address of the maker + /// @param toAddress New address of the maker + function updateMakerAddress(address fromAddress, address toAddress) external; + + /// @notice Update the airswap fee percentage + /// @param _airswapFeeBps Airswap fee percentage in basis points + function setAirswapFeeBps(uint256 _airswapFeeBps) external; + + /// @notice Update the maker fee percentage + /// @param _makerFeeBps Maker fee percentage in basis points + function setMakerFeeBps(uint256 _makerFeeBps) external; + + /// @notice Update the amm lp fee percentage + /// @param _ammFeeLpBps Amm Fee Lp in basis points + function setAmmLpFeeBps(uint256 _ammFeeLpBps) external; + + /// @notice Update the bitocracy fee percentage + /// @param _bitocracyFeeBps Bitocracy fee in basis points + function setBitocracyFeeBps(uint256 _bitocracyFeeBps) external; + + /// @notice Swap one token for another. + /// @param _sender Address which is sending the tokens + /// @param _recipient Address to send the resulting tokens after collecting the output fee + /// @param _nonce A one time nonce + /// @param _expiry Date at which the original proposal will expire + /// @param _signerWallet Address of the market maker wallet + /// @param _signerToken Address of the token to convert to + /// @param _signerAmount Amount of resulting token from the conversion + /// @param _totalSenderAmount The amount to be sent before the input fee is collected. + /// @param _v v part of the ECDSA signature + /// @param _r r part of the ECDSA signature + /// @param _s s part of the ECDSA signature + function swap( + address _sender, + address _recipient, + uint256 _nonce, + uint256 _expiry, + address _signerWallet, + address _signerToken, + uint256 _signerAmount, + address _senderToken, + uint256 _totalSenderAmount, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external; +} diff --git a/contracts/integrations/airswap/IAirswapSwapERC20.sol b/contracts/integrations/airswap/IAirswapSwapERC20.sol new file mode 100644 index 000000000..57c62449d --- /dev/null +++ b/contracts/integrations/airswap/IAirswapSwapERC20.sol @@ -0,0 +1,17 @@ +pragma solidity 0.5.17; + +interface IAirswapSwapERC20 { + function swap( + address recipient, + uint256 nonce, + uint256 expiry, + address signerWallet, + address signerToken, + uint256 signerAmount, + address senderToken, + uint256 senderAmount, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/mockup/AirswapERC20Mockup.sol b/contracts/mockup/AirswapERC20Mockup.sol new file mode 100644 index 000000000..c7f7176b7 --- /dev/null +++ b/contracts/mockup/AirswapERC20Mockup.sol @@ -0,0 +1,67 @@ +pragma solidity 0.5.17; + +import "../openzeppelin/IERC20_.sol"; +import "../integrations/airswap/IAirswapSwapERC20.sol"; + +// This contract is only for testing purposes +contract AirswapERC20Mockup is IAirswapSwapERC20 { + int16 public swapCalled = 0; + + address public recipient; + uint256 public nonce; + uint256 public expiry; + address public signerWallet; + address public signerToken; + uint256 public signerAmount; + address public senderToken; + uint256 public senderAmount; + uint8 public v; + bytes32 public r; + bytes32 public s; + + function reset() public { + recipient = address(0); + nonce = 0; + expiry = 0; + signerWallet = address(0); + signerToken = address(0); + signerAmount = 0; + senderToken = address(0); + senderAmount = 0; + v = 0; + r = 0; + s = 0; + + swapCalled = 0; + } + + function swap( + address _recipient, + uint256 _nonce, + uint256 _expiry, + address _signerWallet, + address _signerToken, + uint256 _signerAmount, + address _senderToken, + uint256 _senderAmount, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + recipient = _recipient; + nonce = _nonce; + expiry = _expiry; + signerWallet = _signerWallet; + signerToken = _signerToken; + signerAmount = _signerAmount; + senderToken = _senderToken; + senderAmount = _senderAmount; + v = _v; + r = _r; + s = _s; + + swapCalled++; + + IERC20_(_signerToken).transfer(_recipient, _signerAmount); + } +} diff --git a/tests/airswap/airswap-integration.js b/tests/airswap/airswap-integration.js new file mode 100644 index 000000000..0f465932d --- /dev/null +++ b/tests/airswap/airswap-integration.js @@ -0,0 +1,222 @@ +const { expectRevert, expectEvent, BN, constants } = require("@openzeppelin/test-helpers"); +const { artifacts } = require("hardhat"); + +const AirswapERC20Mockup = artifacts.require("AirswapERC20Mockup"); +const AirswapFeeConnector = artifacts.require("AirswapFeeConnector"); +const TestToken = artifacts.require("TestToken"); + +const { ZERO_ADDRESS } = constants; +const ONE = new BN("1000000000000000000"); +const INITIAL_MINTED = 1000000; +const INITIAL_AMOUNT_BN = ONE.mul(new BN(INITIAL_MINTED)); +const wei = web3.utils.toWei; +const hunEth = new BN(wei("100", "ether")); + +function bnToComparableNumber(bn) { + // round bn to 9 digits + return bn.divRound(new BN("1000000000")).toNumber() / 10 ** 9; +} + +contract("AirSwap Integration", (accounts) => { + let deployerAddress, senderAddress, marketMakerAddress, recipientAddress, feeVaultAddress; + let testToken1, testToken1Address, testToken2, testToken2Address; + let airSwapERC20Mockup, airswapFeeConnector; + let fakeAddress; + + describe.only("AirSwap Integration test", async () => { + before(async () => { + [ + deployerAddress, + senderAddress, + marketMakerAddress, + recipientAddress, + feeVaultAddress, + ] = accounts; + + airSwapERC20Mockup = await AirswapERC20Mockup.new(); + airswapFeeConnector = await AirswapFeeConnector.new(); + + testToken1 = await TestToken.new("TST1", "TST1", 18, INITIAL_AMOUNT_BN); + testToken1Address = testToken1.address; + testToken2 = await TestToken.new("TST2", "TST2", 18, INITIAL_AMOUNT_BN); + testToken2Address = testToken2.address; + + await testToken2.transfer(airSwapERC20Mockup.address, INITIAL_AMOUNT_BN); + }); + + beforeEach(async () => { + await airSwapERC20Mockup.reset(); + fakeAddress = (await TestToken.new("", "", 18, 0)).address; // just a random address + }); + + describe("inputFee", async () => { + const fakeFee = new BN(Math.floor(10 ^ (18 * Math.random))); + it("owner only", async () => { + await expectRevert( + airswapFeeConnector.setInputFee(fakeFee, { from: senderAddress }), + "unauthorized" + ); + }); + it("can be set", async () => { + const { tx } = await airswapFeeConnector.setInputFee(fakeFee); + const fee = await airswapFeeConnector.inputFeeInPoints(); + expect(fee.toString()).to.be.equal(fakeFee.toString()); + await expectEvent.inTransaction(tx, airswapFeeConnector, "InputFeeChangedEvent", { + sender: deployerAddress, + feeInPoints: fakeFee, + }); + }); + }); + + describe("outputFee", async () => { + const fakeFee = new BN(Math.floor(10 ^ (18 * Math.random))); + it("owner only", async () => { + await expectRevert( + airswapFeeConnector.setOutputFee(fakeFee, { from: senderAddress }), + "unauthorized" + ); + }); + it("can be set", async () => { + const { tx } = await airswapFeeConnector.setOutputFee(fakeFee); + const fee = await airswapFeeConnector.outputFeeInPoints(); + expect(fee.toString()).to.be.equal(fakeFee.toString()); + await expectEvent.inTransaction(tx, airswapFeeConnector, "OutputFeeChangedEvent", { + sender: deployerAddress, + feeInPoints: fakeFee, + }); + }); + }); + + describe("feeVaultAddress", async () => { + it("owner only", async () => { + await expectRevert( + airswapFeeConnector.setFeeVaultAddress(fakeAddress, { from: senderAddress }), + "unauthorized" + ); + }); + it("can be set", async () => { + const { tx } = await airswapFeeConnector.setFeeVaultAddress(fakeAddress); + const address = await airswapFeeConnector.feeVaultAddress(); + expect(address).to.be.equal(fakeAddress); + await expectEvent.inTransaction( + tx, + airswapFeeConnector, + "FeeVaultAddressChangedEvent", + { + sender: deployerAddress, + newAddress: fakeAddress, + } + ); + }); + }); + + describe("swapERC20Address", async () => { + it("owner only", async () => { + await expectRevert( + airswapFeeConnector.setSwapERC20Address(fakeAddress, { from: senderAddress }), + "unauthorized" + ); + }); + it("can be set", async () => { + const { tx } = await airswapFeeConnector.setSwapERC20Address(fakeAddress); + const address = await airswapFeeConnector.swapERC20Address(); + expect(address).to.be.equal(fakeAddress); + await expectEvent.inTransaction( + tx, + airswapFeeConnector, + "SwapERC20AddressChangedEvent", + { + sender: deployerAddress, + newAddress: fakeAddress, + } + ); + }); + }); + + describe("happy flow", async () => { + it("swap is successful", async () => { + const fakeNonce = 1; + const fakeExpiry = 1000000000; + const fakeV = 11; + const fakeR = "0x0101010101010101010101010101010101010101010101010101010101010101"; + const fakeS = "0x0202020202020202020202020202020202020202020202020202020202020202"; + + const POINTS = 1000; + const inputFeePoints = 310; // 31% + const outputFeePoints = 281; // 28.1% + const totalInputAmount = 1473; + const outputAmount = 871340; + const expectedInputFee = (totalInputAmount * inputFeePoints) / POINTS; + const inputAmountAfterFee = totalInputAmount - expectedInputFee; + const expectedOutputFee = (outputAmount * outputFeePoints) / POINTS; + const expectedOutputAmountAfterFee = outputAmount - expectedOutputFee; + + await airswapFeeConnector.setSwapERC20Address(airSwapERC20Mockup.address); + await airswapFeeConnector.setFeeVaultAddress(feeVaultAddress); + await airswapFeeConnector.setInputFee(inputFeePoints); + await airswapFeeConnector.setOutputFee(outputFeePoints); + await airSwapERC20Mockup.reset(); + + function numberToBn(n) { + return new BN(wei(n.toString(), "ether")); + } + + // first we need to approve + await testToken1.approve( + airswapFeeConnector.address, + numberToBn(totalInputAmount) + ); + + // then we can convert + const { tx } = await airswapFeeConnector.swap( + deployerAddress, + recipientAddress, + fakeNonce, + fakeExpiry, + marketMakerAddress, + testToken2Address, + numberToBn(outputAmount), + testToken1Address, + numberToBn(totalInputAmount), + fakeV, + fakeR, + fakeS + ); + + await expectEvent.inTransaction(tx, airswapFeeConnector, "SwapEvent", { + sender: deployerAddress, + recipient: recipientAddress, + sendToken: testToken1Address, + sendAmount: numberToBn(totalInputAmount), + inputFee: numberToBn(expectedInputFee), + receiveToken: testToken2Address, + receiveAmount: numberToBn(expectedOutputAmountAfterFee), + outputFee: numberToBn(expectedOutputFee), + }); + + const actualRecipientBalance = await testToken2.balanceOf(recipientAddress); + expect(bnToComparableNumber(actualRecipientBalance)).to.equal( + expectedOutputAmountAfterFee + ); + + const actualInputFeeCollected = await testToken1.balanceOf(feeVaultAddress); + expect(bnToComparableNumber(actualInputFeeCollected)).to.equal(expectedInputFee); + + const actualOutputFeeCollected = await testToken2.balanceOf(feeVaultAddress); + expect(bnToComparableNumber(actualOutputFeeCollected)).to.equal(expectedOutputFee); + + const actualSenderBalance = await testToken1.balanceOf(deployerAddress); + expect(bnToComparableNumber(actualSenderBalance)).to.equal( + INITIAL_MINTED - totalInputAmount + ); + + expect((await airSwapERC20Mockup.swapCalled()).toNumber()).is.equal(1); + expect((await airSwapERC20Mockup.v()).toNumber()).is.equal(fakeV); + expect((await airSwapERC20Mockup.r()).toString()).is.equal(fakeR); + expect((await airSwapERC20Mockup.s()).toString()).is.equal(fakeS); + expect((await airSwapERC20Mockup.nonce()).toNumber()).is.equal(fakeNonce); + expect((await airSwapERC20Mockup.expiry()).toNumber()).is.equal(fakeExpiry); + }); + }); + }); +});