├── requirements.txt ├── .gitattributes ├── interfaces ├── IChild.sol ├── ICHI.sol ├── IGST.sol ├── UniswapFactoryInterface.sol ├── ILGTRelayer.sol ├── ILGT.sol ├── ILiquidERC20.sol └── UniswapExchangeInterface.sol ├── thesis └── LiquidGasToken_Nadler2020.pdf ├── .gitignore ├── tests ├── integration │ ├── test_gas_refund.py │ ├── conftest.py │ └── test_trading_fee.py ├── unit │ ├── erc20 │ │ ├── test_deploy_mint.py │ │ ├── test_transfer.py │ │ └── test_approve_transferFrom.py │ ├── lgt │ │ ├── test_mint.py │ │ ├── test_free.py │ │ ├── test_mint_to_liquidity.py │ │ ├── test_mint_to_sell.py │ │ ├── test_deploy.py │ │ └── test_buy_and_free.py │ └── liquidity │ │ ├── test_add_liquidity.py │ │ ├── test_remove_liquidity.py │ │ ├── test_transfer_liquidity.py │ │ ├── test_sell_lgt.py │ │ └── test_buy_lgt.py ├── conftest.py └── relayer │ ├── test_call_auto.py │ ├── test_call_max.py │ ├── conftest.py │ └── test_call_fixed.py ├── scripts ├── lgt.py ├── modifier.py ├── all_gts.py └── benchmarks │ ├── benchmarks.json │ ├── gas_token_comparison.py │ └── gas_benchmarks.py ├── brownie-config.yaml ├── contracts ├── TestModifier.sol ├── LgtHelper.sol ├── LGTRelayer.sol ├── ERC20PointerSupply.sol ├── LiquidGasToken.sol └── LiquidERC20.sol ├── LICENSE ├── .github └── workflows │ └── main.yaml └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.9.2,<2.0.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /interfaces/IChild.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | interface IChild { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /thesis/LiquidGasToken_Nadler2020.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matnad/liquid-gas-token/HEAD/thesis/LiquidGasToken_Nadler2020.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea/ 3 | .history 4 | .hypothesis/ 5 | build/ 6 | reports/ 7 | notebooks/ 8 | archive/ 9 | 10 | contracts/LGTDeployer.sol 11 | contracts/TestHelper.sol 12 | contracts/GST.sol -------------------------------------------------------------------------------- /tests/integration/test_gas_refund.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | 3 | 4 | DEADLINE = 999999999999 5 | 6 | 7 | def test_gas_refund(helper, accounts): 8 | """ Test if a gas reduction is achieved by burning tokens """ 9 | tx = helper.burnBuyAndFree(1000000, 25, {'from': accounts[0], 'value': "1 ether"}) 10 | assert tx.gas_used < 700000 11 | -------------------------------------------------------------------------------- /interfaces/ICHI.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | import "OpenZeppelin/openzeppelin-contracts@3.0.1/contracts/token/ERC20/IERC20.sol"; 4 | 5 | interface ICHI is IERC20 { 6 | function free(uint256 value) external returns (uint256); 7 | function freeFrom(address from, uint256 value) external returns (uint256); 8 | function mint(uint256 value) external; 9 | } 10 | -------------------------------------------------------------------------------- /interfaces/IGST.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | import "OpenZeppelin/openzeppelin-contracts@3.0.1/contracts/token/ERC20/IERC20.sol"; 4 | 5 | interface IGST is IERC20 { 6 | function free(uint256 tokenAmount) external returns (bool success); 7 | function freeFrom(address from, uint256 value) external returns (bool success); 8 | function mint(uint256 value) external; 9 | } 10 | -------------------------------------------------------------------------------- /scripts/lgt.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | 3 | from archive.deploy_lgt import deploy_lgt 4 | 5 | 6 | def main(): 7 | """Deploys a funded and approved LGT.""" 8 | lgt = deploy_lgt() 9 | 10 | helper = accounts[0].deploy(LgtHelper) 11 | 12 | for i in range(2): 13 | lgt.approve(helper, 2**256-1, {'from': accounts[i]}) 14 | 15 | return lgt, helper, accounts[1] 16 | # lgt, h, me = run("lgt") -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | # automatically fetch contract sources from Etherscan 2 | autofetch_sources: true 3 | 4 | networks: 5 | development: 6 | gas_price: 0 7 | cmd_settings: 8 | unlock: 0x7E1E3334130355799F833ffec2D731BCa3E68aF6 9 | 10 | compiler: 11 | solc: 12 | optimizer: 13 | enabled: true 14 | runs: 1000000 15 | 16 | dependencies: 17 | - OpenZeppelin/openzeppelin-contracts@3.0.1 18 | 19 | reports: 20 | exclude_contracts: 21 | - SafeMath 22 | -------------------------------------------------------------------------------- /tests/unit/erc20/test_deploy_mint.py: -------------------------------------------------------------------------------- 1 | # import pytest 2 | # from brownie import * 3 | 4 | 5 | def test_deploy_lgt(lgt): 6 | assert lgt.name() == "Liquid Gas Token" 7 | assert lgt.symbol() == "LGT" 8 | assert lgt.decimals() == 0 9 | 10 | 11 | def test_mint_lgt(lgt, accounts): 12 | lgt.mint(1, {'from': accounts[1]}) 13 | lgt.mint(2, {'from': accounts[2]}) 14 | assert lgt.balanceOf(accounts[1]) == 1 15 | assert lgt.balanceOf(accounts[2]) == 2 16 | assert lgt.totalSupply() == 34 17 | -------------------------------------------------------------------------------- /scripts/modifier.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | 3 | from archive.deploy_lgt import deploy_lgt 4 | 5 | 6 | def main(): 7 | lgt = deploy_lgt() 8 | for _ in range(6): 9 | lgt.mint(100, {'from': accounts[1]}) 10 | lgt.addLiquidity(0, 500, 99999999999, {'from': accounts[1], 'value': "0.1 ether"}) 11 | lgt.transfer(accounts[0], 10, {'from': accounts[1]}) 12 | 13 | helper = TestModifier.deploy({'from': accounts[1]}) 14 | 15 | 16 | return lgt, helper, accounts[0] 17 | 18 | # lgt, h, me = run("modifier") -------------------------------------------------------------------------------- /tests/unit/lgt/test_mint.py: -------------------------------------------------------------------------------- 1 | def test_mint(lgt, accounts): 2 | initial_tokens = lgt.balanceOf(accounts[5]) 3 | lgt.mint(10, {'from': accounts[5]}) 4 | assert initial_tokens + 10 == lgt.balanceOf(accounts[5]) 5 | 6 | 7 | def test_mint_for(lgt, accounts): 8 | initial_tokens5 = lgt.balanceOf(accounts[5]) 9 | initial_tokens6 = lgt.balanceOf(accounts[6]) 10 | lgt.mintFor(10, accounts[6], {'from': accounts[5]}) 11 | assert initial_tokens5 == lgt.balanceOf(accounts[5]) 12 | assert initial_tokens6 + 10 == lgt.balanceOf(accounts[6]) 13 | -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="module") 5 | def int_lgt(lgt, accounts): 6 | lgt.mint(50, {'from': accounts[0]}) 7 | lgt.addLiquidity(1, 51, 99999999999, {'from': accounts[0], 'value': "0.05 ether"}) 8 | lgt.mint(80, {'from': accounts[1]}) 9 | lgt.addLiquidity(1, 50, 99999999999, {'from': accounts[1], 'value': "0.049 ether"}) 10 | yield lgt 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def helper(int_lgt, accounts, LgtHelper): 15 | helper = accounts[0].deploy(LgtHelper) 16 | for i in range(3): 17 | int_lgt.approve(helper, 2**256-1, {'from': accounts[i]}) 18 | yield helper 19 | -------------------------------------------------------------------------------- /interfaces/UniswapFactoryInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | interface UniswapFactoryInterface { 4 | // Public Variables 5 | // address public exchangeTemplate; 6 | // uint256 public tokenCount; 7 | // Create Exchange 8 | function createExchange(address token) external returns (address exchange); 9 | // Get Exchange and Token Info 10 | function getExchange(address token) external view returns (address exchange); 11 | function getToken(address exchange) external view returns (address token); 12 | function getTokenWithId(uint256 tokenId) external view returns (address token); 13 | // Never use 14 | function initializeFactory(address template) external; 15 | } -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope="function", autouse=True) 8 | def isolate(fn_isolation): 9 | pass 10 | 11 | 12 | @pytest.fixture(scope="module") 13 | def lgt(LiquidGasToken, LGTDeployer, accounts): 14 | salt = "0x23ad710e5baee63bb004d962a84d3922e236c107944f2efe53e42d51e6d6f121" 15 | coffee = accounts.add("redacted") 16 | d = coffee.deploy(LGTDeployer) # 0x8EE26bA26c87Beb287eB71245ADEf44ede1bF190 17 | accounts[0].transfer("0x000000000000C1CB11D5c062901F32D06248CE48", "0.001 ether") 18 | d.deploy(salt) 19 | lgt = LiquidGasToken.at("0x000000000000C1CB11D5c062901F32D06248CE48") 20 | lgt.mint(30, {'from': accounts[0]}) 21 | yield lgt 22 | -------------------------------------------------------------------------------- /tests/unit/erc20/test_transfer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | 4 | 5 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 6 | 7 | 8 | def test_transfer(lgt, accounts): 9 | assert lgt.balanceOf(accounts[0]) == 30 10 | lgt.transfer(accounts[1], 10, {'from': accounts[0]}) 11 | assert lgt.balanceOf(accounts[1]) == 10 12 | assert lgt.balanceOf(accounts[0]) == 20 13 | 14 | 15 | def test_transfer_exceed_reverts(lgt, accounts): 16 | with brownie.reverts("ERC20: transfer exceeds balance"): 17 | lgt.transfer(accounts[1], 10, {'from': accounts[2]}) 18 | 19 | 20 | def test_transfer_zero_reverts(lgt, accounts): 21 | with brownie.reverts("ERC20: transfer to zero address"): 22 | lgt.transfer(ZERO_ADDRESS, 10, {'from': accounts[0]}) 23 | -------------------------------------------------------------------------------- /tests/relayer/test_call_auto.py: -------------------------------------------------------------------------------- 1 | DEADLINE = 99999999999 2 | 3 | 4 | def test_forward_auto_no_gas_cost_refund(liquid_lgt, relayer, helper, accounts): 5 | """ 6 | Burn 1 million gas, expected to buy 25 tokens if gas price is high enough 7 | Gas cost is 0, so no purchase will happen, but the ether should be refunded. 8 | """ 9 | initial_tokens = liquid_lgt.poolTokenReserves() 10 | initial_balance = accounts[0].balance() 11 | expected_eth_sold = liquid_lgt.getEthToTokenOutputPrice(25) 12 | calldata = "0x4ad5d16f00000000000000000000000000000000000000000000000000000000000f4240" 13 | tx = relayer.forwardAuto(helper, 0, calldata, {'from': accounts[0], 'value': "1 ether"}) 14 | assert tx.return_value == (25, expected_eth_sold) 15 | assert tx.gas_price == 0 16 | assert tx.gas_used > 1e6 17 | assert initial_tokens == liquid_lgt.poolTokenReserves() 18 | assert initial_balance == accounts[0].balance() 19 | -------------------------------------------------------------------------------- /contracts/TestModifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.9; 3 | 4 | import "../interfaces/ILGT.sol"; 5 | 6 | contract TestModifier { 7 | uint private c = 1; 8 | address public constant LGT = 0x00000000007475142d6329FC42Dc9684c9bE6cD0; 9 | 10 | modifier refund() { 11 | uint initialGas = gasleft(); 12 | _; 13 | uint t = (initialGas - gasleft() + 19560) / 41717; 14 | if (t > 0) { 15 | c = ILGT(LGT).getEthToTokenOutputPrice(t); 16 | if (c < ((17717 * t) - 19560) * tx.gasprice) { 17 | ILGT(LGT).buyAndFree{value: msg.value}(t, now + 1, msg.sender); 18 | } 19 | } 20 | } 21 | 22 | function expensiveRefund(uint n) public payable refund { 23 | for (uint i = 0; i < n; i++) { 24 | c = i; 25 | } 26 | } 27 | 28 | function expensiveNoRefund(uint n) public { 29 | for (uint i = 0; i < n; i++) { 30 | c = i; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /interfaces/ILGTRelayer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | interface ILGTRelayer { 4 | 5 | // Forward calldata and `value` to `destination` and buy exactly `tokenAmount` 6 | function forward(uint256 tokenAmount, uint256 deadline, address destination, uint256 value, bytes memory data) 7 | external payable returns (uint256 ethSold); 8 | 9 | // Forward calldata and `value` to `destination` and buy as much tokens as possible with the passed ether 10 | function forwardMax(uint256 deadline, address destination, uint256 value, bytes memory data) 11 | external payable returns (uint256 tokensBought); 12 | 13 | // Forward calldata and `value` to `destination`. Calculate optimal amount of tokens to buy and check 14 | // if it is profitable at the current gas price. If all is good, buy tokens and free them. 15 | // This is not exact science, use with caution. 16 | function forwardAuto(address destination, uint256 value, bytes memory data) 17 | external payable returns (uint256 optimalTokens, uint256 buyCost); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matthias Nadler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/relayer/test_call_max.py: -------------------------------------------------------------------------------- 1 | DEADLINE = 99999999999 2 | 3 | 4 | def test_call_max_no_value(liquid_lgt, relayer, storage, accounts): 5 | initial_tokens = liquid_lgt.poolTokenReserves() 6 | calldata = "60fe47b1000000000000000000000000000000000000000000000000000000000000000f" 7 | expected_tokens = liquid_lgt.getEthToTokenInputPrice("0.003 ether") 8 | assert expected_tokens > 0 9 | tx = relayer.forwardMax(DEADLINE, storage, 0, calldata, {'from': accounts[0], 'value': "0.003 ether"}) 10 | assert tx.return_value == expected_tokens 11 | assert storage.get() == "0xf" 12 | assert initial_tokens - expected_tokens == liquid_lgt.poolTokenReserves() 13 | 14 | 15 | def test_call_max_with_value(liquid_lgt, relayer, storage, accounts): 16 | """ Buy tokens for 0.004 ether and send 0.002 ether with the call. """ 17 | initial_tokens = liquid_lgt.poolTokenReserves() 18 | calldata = "bc25bd200000000000000000000000000000000000000000000000000000000000000000" 19 | expected_tokens = liquid_lgt.getEthToTokenInputPrice("0.004 ether") 20 | assert expected_tokens > 0 21 | tx = relayer.forwardMax(DEADLINE, storage, "0.002 ether", calldata, {'from': accounts[0], 'value': "0.006 ether"}) 22 | assert tx.return_value == expected_tokens 23 | assert storage.get() == "0.002 ether" 24 | assert initial_tokens - expected_tokens == liquid_lgt.poolTokenReserves() 25 | -------------------------------------------------------------------------------- /scripts/all_gts.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | 3 | 4 | def main(): 5 | """ 6 | Deploys, funds and approves GST2, CHI and LGT. 7 | Must be run on mainnet fork. 8 | """ 9 | rpc.reset() 10 | # Deploy and fund LGT 11 | salt = "0x23ad710e5baee63bb004d962a84d3922e236c107944f2efe53e42d51e6d6f121" 12 | coffee = accounts.add("redacted") 13 | accounts[0].transfer(coffee, "1 ether") 14 | d = coffee.deploy(LGTDeployer) # 0x8EE26bA26c87Beb287eB71245ADEf44ede1bF190 15 | coffee.transfer("0x000000000000C1CB11D5c062901F32D06248CE48", "0.001 ether") 16 | d.deploy(salt) 17 | lgt = LiquidGasToken.at("0x000000000000C1CB11D5c062901F32D06248CE48") 18 | lgt.mint(110, {'from': accounts[0]}) 19 | lgt.addLiquidity(1, 50, 999999999999999, {'from': accounts[0], 'value': "0.049 ether"}) 20 | lgt.mint(70, {'from': accounts[1]}) 21 | lgt.addLiquidity(1, 50, 999999999999999, {'from': accounts[1], 'value': "0.049 ether"}) 22 | lgt.mint(50, {'from': accounts[2]}) 23 | 24 | # Load GST2 25 | gst = interface.IGST("0x0000000000b3F879cb30FE243b4Dfee438691c04") 26 | gst.mint(60, {'from': accounts[0]}) 27 | gst.mint(20, {'from': accounts[1]}) 28 | gst.mint(50, {'from': accounts[2]}) 29 | 30 | # Load CHI 31 | chi = interface.ICHI("0x0000000000004946c0e9F43F4Dee607b0eF1fA1c") 32 | # chi = Contract.from_explorer("0x0000000000004946c0e9F43F4Dee607b0eF1fA1c") 33 | chi.mint(60, {'from': accounts[0]}) 34 | chi.mint(20, {'from': accounts[1]}) 35 | chi.mint(50, {'from': accounts[2]}) 36 | 37 | # deploy helper contract and approve it 38 | helper = accounts[0].deploy(LgtHelper) 39 | for i in range(5): 40 | lgt.approve(helper, 2**256-1, {'from': accounts[i]}) 41 | gst.approve(helper, 2 ** 256 - 1, {'from': accounts[i]}) 42 | chi.approve(helper, 2 ** 256 - 1, {'from': accounts[i]}) 43 | 44 | return lgt, gst, chi, helper, accounts[2] 45 | # lgt, gst, chi, h, me = run("all_gts") -------------------------------------------------------------------------------- /tests/integration/test_trading_fee.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | import pytest 3 | 4 | 5 | DEADLINE = 999999999999 6 | 7 | 8 | def test_fee(int_lgt, rpc, accounts): 9 | """ Test if fee structure of liquidity pool is working as intended. """ 10 | # setup and verify liquidity shares 11 | provider = accounts[0] 12 | spender = accounts[4] 13 | provider_absolute_share = int_lgt.poolBalanceOf(provider) 14 | total_shares = int_lgt.poolTotalSupply() 15 | assert provider_absolute_share == "0.05 ether" 16 | assert total_shares == "0.1 ether" 17 | provider_share = provider_absolute_share / total_shares 18 | assert provider_share == 0.5 19 | initial_token_res = int_lgt.poolTokenReserves() 20 | initial_eth_res = int_lgt.balance() 21 | 22 | # Check how much tokens and ether we would get now 23 | rpc.snapshot() 24 | tx = int_lgt.removeLiquidity("0.05 ether", 1, 1, DEADLINE, {'from': provider}) 25 | initial_withdraw = tx.return_value 26 | assert initial_withdraw == ("0.05 ether", 51) 27 | rpc.revert() 28 | 29 | # process some transactions 30 | int_lgt.buyAndFree(10, DEADLINE, spender, {'from': spender, 'value': "1 ether"}) 31 | int_lgt.ethToTokenSwapOutput(30, DEADLINE, {'from': spender, 'value': "1 ether"}) 32 | int_lgt.mintToSell(40, 1, DEADLINE, {'from': spender}) 33 | # -> same amount of tokens in the exchange, but should have more eth 34 | assert initial_token_res == int_lgt.poolTokenReserves() 35 | assert initial_eth_res < int_lgt.balance() 36 | 37 | # Remove Liquidity and realize fee profits 38 | current_balance = int_lgt.balance() 39 | assert int_lgt.poolBalanceOf(provider) == "0.05 ether" 40 | tx = int_lgt.removeLiquidity("0.05 ether", 1, 1, DEADLINE, {'from': provider}) 41 | eth_withdrawn, tokens_withdrawn = tx.return_value 42 | # assert the provider made a profit equal to his share 43 | assert tokens_withdrawn == 51 44 | assert eth_withdrawn > "0.05 ether" 45 | assert eth_withdrawn * 10 ** -18 == pytest.approx(current_balance * provider_share * 10 ** -18, abs=0.0001) 46 | -------------------------------------------------------------------------------- /contracts/LgtHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.9; 3 | 4 | import "../interfaces/ILGT.sol"; 5 | import "../interfaces/IGST.sol"; 6 | import "../interfaces/ICHI.sol"; 7 | 8 | contract LgtHelper { 9 | uint256 public c = 1; 10 | 11 | ILGT public constant lgt = ILGT(0x000000000000C1CB11D5c062901F32D06248CE48); 12 | ICHI public constant chi = ICHI(0x0000000000004946c0e9F43F4Dee607b0eF1fA1c); 13 | IGST public constant gst = IGST(0x0000000000b3F879cb30FE243b4Dfee438691c04); 14 | 15 | function burnGas(uint256 burn) public returns (uint256 burned) { 16 | uint256 start = gasleft(); 17 | assert(start > burn + 200); 18 | uint256 end = start - burn; 19 | while (gasleft() > end + 5000) { 20 | c++; 21 | } 22 | while(gasleft() > end) {} 23 | burned = start - gasleft(); 24 | } 25 | 26 | function burnAndFree(uint256 burn, uint256 tokenAmount) public returns (bool success) { 27 | burnGas(burn); 28 | return lgt.freeFrom(tokenAmount, msg.sender); 29 | } 30 | 31 | function burnAndFreeGST(uint256 burn, uint256 tokenAmount) public returns (bool success) { 32 | burnGas(burn); 33 | return gst.freeFrom(msg.sender, tokenAmount); 34 | } 35 | 36 | function burnAndFreeCHI(uint256 burn, uint256 tokenAmount) public returns (uint256) { 37 | burnGas(burn); 38 | return chi.freeFrom(msg.sender, tokenAmount); 39 | } 40 | 41 | function burnBuyAndFree(uint256 burn, uint256 tokenAmount) 42 | public payable returns (uint256 ethSold) 43 | { 44 | burnGas(burn); 45 | return lgt.buyAndFree{value: msg.value}(tokenAmount, now, msg.sender); 46 | } 47 | 48 | function burnBuyAndFreeOpt(uint256 burn, uint256 tokenAmount) public payable { 49 | burnGas(burn); 50 | lgt.buyAndFree22457070633{value: msg.value}(tokenAmount); 51 | } 52 | 53 | function burnBuyMaxAndFree(uint256 burn) 54 | public payable returns (uint256 ethSold) 55 | { 56 | burnGas(burn); 57 | return lgt.buyMaxAndFree{value: msg.value}(now); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/benchmarks/benchmarks.json: -------------------------------------------------------------------------------- 1 | { 2 | "free": { 3 | "buyAndFree": { 4 | "args": [ 5 | 999999999999999, 6 | "account" 7 | ], 8 | "gas_used": [ 9 | 18336, 10 | 59321, 11 | 109089, 12 | 223261 13 | ], 14 | "value": "getEthToTokenOutputPrice" 15 | }, 16 | "buyAndFree22457070633": { 17 | "args": [], 18 | "gas_used": [ 19 | 17926, 20 | 58911, 21 | 108679, 22 | 222851 23 | ], 24 | "value": "getEthToTokenOutputPrice" 25 | }, 26 | "free": { 27 | "args": [], 28 | "gas_used": [ 29 | 22687, 30 | 63672, 31 | 22440, 32 | 22440 33 | ] 34 | }, 35 | "freeFrom": { 36 | "args": [ 37 | "account" 38 | ], 39 | "gas_used": [ 40 | 23816, 41 | 23816, 42 | 22839, 43 | 22839 44 | ] 45 | } 46 | }, 47 | "mint": { 48 | "mint": { 49 | "args": [], 50 | "gas_used": [ 51 | 75578, 52 | 586424, 53 | 1205346, 54 | 2627026 55 | ] 56 | }, 57 | "mintFor": { 58 | "args": [ 59 | "account" 60 | ], 61 | "gas_used": [ 62 | 75992, 63 | 586838, 64 | 1205760, 65 | 2627440 66 | ] 67 | }, 68 | "mintToLiquidity": { 69 | "args": [ 70 | 1, 71 | 999999999999999, 72 | "account" 73 | ], 74 | "gas_used": [ 75 | 88337, 76 | 599183, 77 | 1218105, 78 | 2639785 79 | ], 80 | "value": "2 ether" 81 | }, 82 | "mintToSell": { 83 | "args": [ 84 | 1, 85 | 999999999999999 86 | ], 87 | "gas_used": [ 88 | 74366, 89 | 585212, 90 | 1204134, 91 | 2625814 92 | ] 93 | }, 94 | "mintToSellTo": { 95 | "args": [ 96 | 1, 97 | 999999999999999, 98 | "account" 99 | ], 100 | "gas_used": [ 101 | 74699, 102 | 585545, 103 | 1204467, 104 | 2626147 105 | ] 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /tests/unit/lgt/test_free.py: -------------------------------------------------------------------------------- 1 | """ 2 | These tests don't verify if gas is refunded, only if tokens are burned. 3 | This is done with integration tests. 4 | """ 5 | 6 | 7 | def test_free(lgt, accounts): 8 | assert lgt.balanceOf(accounts[0]) == 30 9 | tx = lgt.free(10, {'from': accounts[0]}) 10 | assert tx.return_value 11 | assert lgt.balanceOf(accounts[0]) == 20 12 | 13 | 14 | def test_free_zero(lgt, accounts): 15 | assert lgt.balanceOf(accounts[0]) == 30 16 | tx = lgt.free(0, {'from': accounts[0]}) 17 | assert tx.return_value 18 | assert lgt.balanceOf(accounts[0]) == 30 19 | 20 | 21 | def test_free_from(lgt, accounts): 22 | owner, spender = accounts[:2] 23 | assert lgt.balanceOf(owner) == 30 24 | lgt.approve(spender, 11, {'from': owner}) 25 | assert lgt.allowance(owner, spender) == 11 26 | tx = lgt.freeFrom(10, owner, {'from': spender}) 27 | assert tx.return_value 28 | assert lgt.balanceOf(owner) == 20 29 | assert lgt.allowance(owner, spender) == 1 30 | 31 | 32 | def test_free_from_zero(lgt, accounts): 33 | owner, spender = accounts[:2] 34 | assert lgt.balanceOf(owner) == 30 35 | lgt.approve(spender, 11, {'from': owner}) 36 | assert lgt.allowance(owner, spender) == 11 37 | tx = lgt.freeFrom(0, owner, {'from': spender}) 38 | assert tx.return_value 39 | assert lgt.balanceOf(owner) == 30 40 | assert lgt.allowance(owner, spender) == 11 41 | 42 | 43 | def test_no_spender_balance_fails(lgt, accounts): 44 | owner, spender = accounts[:2] 45 | tx = lgt.free(10, {'from': spender}) 46 | assert not tx.return_value 47 | 48 | 49 | def test_no_allowance_fails(lgt, accounts): 50 | owner, spender = accounts[:2] 51 | tx = lgt.freeFrom(10, owner, {'from': spender}) 52 | assert not tx.return_value 53 | 54 | 55 | def test_insufficient_allowance_fails(lgt, accounts): 56 | owner, spender = accounts[:2] 57 | lgt.approve(spender, 5, {'from': owner}) 58 | tx = lgt.freeFrom(10, owner, {'from': spender}) 59 | assert not tx.return_value 60 | 61 | 62 | def test_insufficient_owner_balance_fails(lgt, accounts): 63 | owner, spender = accounts[:2] 64 | lgt.approve(spender, 100, {'from': owner}) 65 | tx = lgt.freeFrom(50, owner, {'from': spender}) 66 | assert not tx.return_value 67 | -------------------------------------------------------------------------------- /interfaces/ILGT.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | import "./ILiquidERC20.sol"; 4 | 5 | interface ILGT is ILiquidERC20 { 6 | 7 | // Minting Tokens 8 | function mint(uint256 amount) external; 9 | function mintFor(uint256 amount, address recipient) external; 10 | function mintToLiquidity(uint256 maxTokens, uint256 minLiquidity, uint256 deadline, address recipient) 11 | external payable returns (uint256 tokenAmount, uint256 ethAmount, uint256 liquidityCreated); 12 | function mintToSell(uint256 amount, uint256 minEth, uint256 deadline) 13 | external returns (uint256 ethBought); 14 | function mintToSellTo(uint256 amount, uint256 minEth, uint256 deadline, address payable recipient) 15 | external returns (uint256 ethBought); 16 | 17 | // Freeing Tokens 18 | function free(uint256 amount) external returns (bool success); 19 | function freeFrom(uint256 amount, address owner) external returns (bool success); 20 | 21 | // Buying and Freeing Tokens. 22 | // It is always recommended to check the price for the amount of tokens you intend to buy 23 | // and then send the exact amount of ether. 24 | 25 | // Will refund excess ether and returns 0 instead of reverting on most errors. 26 | function buyAndFree(uint256 amount, uint256 deadline, address payable refundTo) 27 | external payable returns (uint256 ethSold); 28 | 29 | // Spends all ether (no refunds) to buy and free as many tokens as possible. 30 | function buyMaxAndFree(uint256 deadline) 31 | external payable returns (uint256 tokensBought); 32 | 33 | 34 | 35 | // Optimized Functions 36 | // !!! USE AT YOUR OWN RISK !!! 37 | // These functions are gas optimized and intended for experienced users. 38 | // The function names are constructed to have 3 or 4 leading zero bytes 39 | // in the function selector. 40 | // Additionally, all checks have been omitted and need to be done before 41 | // sending the call if desired. 42 | // There are also no return values to further save gas. 43 | // !!! USE AT YOUR OWN RISK !!! 44 | function mintToSell9630191(uint256 amount) external; 45 | function mintToSellTo25630722(uint256 amount, address payable recipient) external; 46 | function buyAndFree22457070633(uint256 amount) external payable; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | WEB3_INFURA_PROJECT_ID: 272d125948404ff8a8dc6a573d2015d7 13 | 14 | jobs: 15 | 16 | Tests: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | 22 | - name: Cache Solidity Installations 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.solcx 26 | key: ${{ runner.os }}-solcx-cache 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v1 30 | 31 | - name: Install Ganache 32 | run: npm install -g ganache-cli@6.9.1 33 | 34 | - name: Setup Python 3.8 35 | uses: actions/setup-python@v1 36 | with: 37 | python-version: 3.8 38 | 39 | - name: Install Requirements 40 | run: pip install -r requirements.txt 41 | 42 | - name: Install Dependencies 43 | run: brownie pm install OpenZeppelin/openzeppelin-contracts@3.0.1 44 | 45 | - name: Run Unit Tests 46 | run: brownie test tests/unit/ -w -n auto 47 | 48 | - name: Run Integration Tests 49 | run: brownie test tests/integration/ -w -n auto 50 | 51 | Benchmarks: 52 | runs-on: ubuntu-latest 53 | 54 | steps: 55 | - uses: actions/checkout@v1 56 | 57 | - name: Cache Solidity Installations 58 | uses: actions/cache@v1 59 | with: 60 | path: ~/.solcx 61 | key: ${{ runner.os }}-solcx-cache 62 | 63 | - name: Setup Node.js 64 | uses: actions/setup-node@v1 65 | 66 | - name: Install Ganache 67 | run: npm install -g ganache-cli@6.9.1 68 | 69 | - name: Setup Python 3.8 70 | uses: actions/setup-python@v1 71 | with: 72 | python-version: 3.8 73 | 74 | - name: Install Requirements 75 | run: pip install -r requirements.txt 76 | 77 | - name: Install Dependencies 78 | run: brownie pm install OpenZeppelin/openzeppelin-contracts@3.0.1 79 | 80 | - name: Run Internal Benchmarks 81 | run: brownie run benchmarks/gas_benchmarks --network mainnet-fork 82 | 83 | - name: Run Comparative Benchmarks 84 | run: brownie run benchmarks/gas_token_comparison --network mainnet-fork 85 | -------------------------------------------------------------------------------- /tests/unit/erc20/test_approve_transferFrom.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | 5 | def test_balance(lgt, accounts): 6 | assert lgt.balanceOf(accounts[0]) == 30 7 | 8 | 9 | def test_approve(lgt, accounts): 10 | lgt.approve(accounts[1], 50, {'from': accounts[0]}) 11 | assert lgt.allowance(accounts[0], accounts[1]) == 50 12 | assert lgt.allowance(accounts[0], accounts[2]) == 0 13 | 14 | lgt.approve(accounts[1], 60, {'from': accounts[0]}) 15 | assert lgt.allowance(accounts[0], accounts[1]) == 60 16 | 17 | 18 | def test_increase_approval(lgt, accounts): 19 | lgt.approve(accounts[1], 20, {'from': accounts[0]}) 20 | lgt.increaseAllowance(accounts[1], 10, {'from': accounts[0]}) 21 | assert lgt.allowance(accounts[0], accounts[1]) == 30 22 | 23 | 24 | def test_increase_approval_reverts(lgt, accounts): 25 | lgt.approve(accounts[1], 30, {'from': accounts[0]}) 26 | with brownie.reverts("SafeMath: addition overflow"): 27 | lgt.increaseAllowance(accounts[1], 2**256-1, {'from': accounts[0]}) 28 | assert lgt.allowance(accounts[0], accounts[1]) == 30 29 | 30 | 31 | def test_decrease_approval(lgt, accounts): 32 | lgt.approve(accounts[1], 40, {'from': accounts[0]}) 33 | lgt.decreaseAllowance(accounts[1], 5, {'from': accounts[0]}) 34 | assert lgt.allowance(accounts[0], accounts[1]) == 35 35 | 36 | 37 | def test_decrease_approval_reverts(lgt, accounts): 38 | lgt.approve(accounts[1], 50, {'from': accounts[0]}) 39 | with brownie.reverts("ERC20: allowance below zero"): 40 | lgt.decreaseAllowance(accounts[1], 60, {'from': accounts[0]}) 41 | assert lgt.allowance(accounts[0], accounts[1]) == 50 42 | 43 | 44 | def test_transfer_from(lgt, accounts): 45 | """Transfer lgts with transferFrom""" 46 | lgt.approve(accounts[1], 15, {'from': accounts[0]}) 47 | lgt.transferFrom(accounts[0], accounts[2], 5, {'from': accounts[1]}) 48 | 49 | assert lgt.balanceOf(accounts[2]) == 5 50 | assert lgt.balanceOf(accounts[1]) == 0 51 | assert lgt.balanceOf(accounts[0]) == 25 52 | assert lgt.allowance(accounts[0], accounts[1]) == 10 53 | 54 | 55 | @pytest.mark.parametrize('idx', [0, 1, 2]) 56 | def test_transfer_from_reverts(lgt, accounts, idx): 57 | """transerFrom should revert""" 58 | with brownie.reverts("ERC20: exceeds allowance"): 59 | lgt.transferFrom(accounts[0], accounts[2], 20, {'from': accounts[idx]}) 60 | -------------------------------------------------------------------------------- /tests/relayer/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="module") 5 | def liquid_lgt(lgt, accounts): 6 | lgt.mint(50, {'from': accounts[0]}) 7 | lgt.addLiquidity(1, 51, 99999999999, {'from': accounts[0], 'value': "0.05 ether"}) 8 | lgt.mint(80, {'from': accounts[1]}) 9 | lgt.addLiquidity(1, 50, 99999999999, {'from': accounts[1], 'value': "0.049 ether"}) 10 | yield lgt 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def relayer(accounts, LGTRelayer): 15 | yield accounts[0].deploy(LGTRelayer) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def helper(accounts, LgtHelper): 20 | yield accounts[0].deploy(LgtHelper) 21 | 22 | 23 | @pytest.fixture(scope="module") 24 | def storage(accounts, liquid_lgt, Contract): 25 | storage_bytecode = "0x6080604052600560005534801561001557600080fd5b5060e3806100246000396000f3fe60806040526004361060305760003560e01c806360fe47b11460355780636d4ce63c14605d578063bc25bd20146081575b600080fd5b348015604057600080fd5b50605b60048036036020811015605557600080fd5b5035609b565b005b348015606857600080fd5b50606f60a0565b60408051918252519081900360200190f35b605b60048036036020811015609557600080fd5b503560a6565b600055565b60005490565b503460005556fea2646970667358221220f24d2143601b1aae859f1a7cd42f132a90acc23593aa210b7c23e868eb26601264736f6c63430006090033" 26 | storage_abi = [ 27 | { 28 | "inputs": [], 29 | "name": "get", 30 | "outputs": [ 31 | { 32 | "internalType": "uint256", 33 | "name": "", 34 | "type": "uint256" 35 | } 36 | ], 37 | "stateMutability": "view", 38 | "type": "function" 39 | }, 40 | { 41 | "inputs": [ 42 | { 43 | "internalType": "uint256", 44 | "name": "_x", 45 | "type": "uint256" 46 | } 47 | ], 48 | "name": "set", 49 | "outputs": [], 50 | "stateMutability": "nonpayable", 51 | "type": "function" 52 | }, 53 | { 54 | "inputs": [ 55 | { 56 | "internalType": "uint256", 57 | "name": "_x", 58 | "type": "uint256" 59 | } 60 | ], 61 | "name": "setPayable", 62 | "outputs": [], 63 | "stateMutability": "payable", 64 | "type": "function" 65 | } 66 | ] 67 | tx = liquid_lgt.deploy(0, 99999999999999, storage_bytecode, {'from': accounts[0], 'value': "1 ether"}) 68 | address = tx.return_value 69 | yield Contract.from_abi(name="Storage", address=address, abi=storage_abi, owner=accounts[0]) 70 | -------------------------------------------------------------------------------- /interfaces/ILiquidERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | import "OpenZeppelin/openzeppelin-contracts@3.0.1/contracts/token/ERC20/IERC20.sol"; 4 | 5 | interface ILiquidERC20 is IERC20 { 6 | 7 | // Price Query Functions 8 | function getEthToTokenInputPrice(uint256 ethSold) external view returns(uint256 tokensBought); 9 | function getEthToTokenOutputPrice(uint256 tokensBought) external view returns (uint256 ethSold); 10 | function getTokenToEthInputPrice(uint256 tokensSold) external view returns (uint256 ethBought); 11 | function getTokenToEthOutputPrice(uint256 ethBought) external view returns (uint256 tokensSold); 12 | 13 | // Liquidity Pool 14 | function poolTotalSupply() external view returns (uint256); 15 | function poolTokenReserves() external view returns (uint256); 16 | function poolBalanceOf(address account) external view returns (uint256); 17 | function poolTransfer(address recipient, uint256 amount) external returns (bool); 18 | function addLiquidity(uint256 minLiquidity, uint256 maxTokens, uint256 deadline) 19 | external payable returns (uint256 liquidityCreated); 20 | function removeLiquidity(uint256 amount, uint256 minEth, uint256 minTokens, uint256 deadline) 21 | external returns (uint256 ethAmount, uint256 tokenAmount); 22 | 23 | // Buy Tokens 24 | function ethToTokenSwapInput(uint256 minTokens, uint256 deadline) 25 | external payable returns (uint256 tokensBought); 26 | function ethToTokenTransferInput(uint256 minTokens, uint256 deadline, address recipient) 27 | external payable returns (uint256 tokensBought); 28 | function ethToTokenSwapOutput(uint256 tokensBought, uint256 deadline) 29 | external payable returns (uint256 ethSold); 30 | function ethToTokenTransferOutput(uint256 tokensBought, uint256 deadline, address recipient) 31 | external payable returns (uint256 ethSold); 32 | 33 | // Sell Tokens 34 | function tokenToEthSwapInput(uint256 tokensSold, uint256 minEth, uint256 deadline) 35 | external returns (uint256 ethBought); 36 | function tokenToEthTransferInput(uint256 tokensSold, uint256 minEth, uint256 deadline, address payable recipient) 37 | external returns (uint256 ethBought); 38 | function tokenToEthSwapOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline) 39 | external returns (uint256 tokensSold); 40 | function tokenToEthTransferOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline, address payable recipient) 41 | external returns (uint256 tokensSold); 42 | 43 | // Events 44 | event AddLiquidity( 45 | address indexed provider, 46 | uint256 indexed eth_amount, 47 | uint256 indexed token_amount 48 | ); 49 | event RemoveLiquidity( 50 | address indexed provider, 51 | uint256 indexed eth_amount, 52 | uint256 indexed token_amount 53 | ); 54 | event TransferLiquidity( 55 | address indexed from, 56 | address indexed to, 57 | uint256 value 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/liquidity/test_add_liquidity.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | 4 | DEADLINE = 9999999999999 5 | 6 | 7 | # TODO: Parametrize 8 | def test_add_liquidity_eth_constraint(lgt, accounts): 9 | assert lgt.poolBalanceOf(accounts[1]) == 0 10 | lgt.mint(15, {'from': accounts[1]}) 11 | tx = lgt.addLiquidity("0.005 ether", 15, DEADLINE, {'from': accounts[1], 'value': "0.005 ether"}) 12 | # tokens = 0.005 / 0.001 + 1 = 6 13 | # Should add 0.005 ether and 6 tokens to liquidity 14 | assert lgt.poolBalanceOf(accounts[1]) == "0.005 ether" 15 | assert tx.return_value == "0.005 ether" 16 | event = tx.events['AddLiquidity'] 17 | assert event["provider"] == accounts[1] 18 | assert event["eth_amount"] == "0.005 ether" 19 | assert event["token_amount"] == 6 20 | assert lgt.poolTotalSupply() == "0.006 ether" 21 | assert lgt.balanceOf(accounts[1]) == 15 - 6 22 | assert lgt.ownedSupply() == 30 + 15 - 6 23 | 24 | 25 | def test_add_liquidity_exact(lgt, accounts): 26 | lgt.mint(9, {'from': accounts[1]}) 27 | tx = lgt.addLiquidity("0.008 ether", 9, DEADLINE, {'from': accounts[1], 'value': "0.008 ether"}) 28 | # tokens = 0.008 / 0.001 + 1 = 9 29 | # Should add 0.008 ether and 9 tokens to liquidity 30 | assert tx.return_value == "0.008 ether" 31 | assert lgt.poolTotalSupply() == "0.009 ether" 32 | assert lgt.balanceOf(accounts[1]) == 0 33 | assert lgt.ownedSupply() == 30 34 | 35 | 36 | def test_add_liquidity_insufficient_lgt(lgt, accounts): 37 | lgt.mint(15, {'from': accounts[1]}) 38 | with brownie.reverts('dev: need more tokens'): 39 | tx = lgt.addLiquidity("0.1 ether", 15, DEADLINE, {'from': accounts[1], 'value': "0.1 ether"}) 40 | assert tx.return_value == 0 41 | # tokens = 0.1 / 0.001 + 1 = 101 42 | # Needs 101 tokens to add 0.1 ether liquidity 43 | assert lgt.poolTotalSupply() == "0.001 ether" 44 | assert lgt.balanceOf(accounts[1]) == 15 45 | 46 | 47 | def test_add_liquidity_insufficient_liquidity(lgt, accounts): 48 | lgt.mint(15, {'from': accounts[1]}) 49 | with brownie.reverts('dev: not enough liquidity can be created'): 50 | tx = lgt.addLiquidity("0.01 ether", 9, DEADLINE, {'from': accounts[1], 'value': "0.008 ether"}) 51 | assert tx.return_value == 0 52 | # Would create 0.008 liquidity, bit minimum 0.01 is requested 53 | assert lgt.poolTotalSupply() == "0.001 ether" 54 | assert lgt.balanceOf(accounts[1]) == 15 55 | 56 | 57 | def test_deadline_reverts(lgt, accounts): 58 | with brownie.reverts('dev: deadline passed'): 59 | lgt.addLiquidity(1, 1, 1, {'from': accounts[1], 'value': "0.008 ether"}) 60 | 61 | 62 | def test_max_tokens_reverts(lgt, accounts): 63 | with brownie.reverts('dev: no tokens to add'): 64 | lgt.addLiquidity(1, 0, DEADLINE, {'from': accounts[1], 'value': "0.008 ether"}) 65 | 66 | 67 | def test_no_min_liq_reverts(lgt, accounts): 68 | with brownie.reverts('dev: no min_liquidity specified'): 69 | lgt.addLiquidity(0, 9, DEADLINE, {'from': accounts[1], 'value': "0.008 ether"}) -------------------------------------------------------------------------------- /tests/relayer/test_call_fixed.py: -------------------------------------------------------------------------------- 1 | from brownie import Wei 2 | 3 | DEADLINE = 99999999999 4 | 5 | 6 | def test_forward_no_value_refund(liquid_lgt, relayer, storage, accounts): 7 | initial_tokens = liquid_lgt.poolTokenReserves() 8 | initial_balance = accounts[0].balance() 9 | calldata = "60fe47b1000000000000000000000000000000000000000000000000000000000000000e" 10 | expected_eth_sold = liquid_lgt.getEthToTokenOutputPrice(4) 11 | tx = relayer.forward(4, DEADLINE, storage, 0, calldata, {'from': accounts[0], 'value': "1 ether"}) 12 | assert tx.return_value == expected_eth_sold 13 | assert storage.get() == "0xe" 14 | assert initial_tokens - 4 == liquid_lgt.poolTokenReserves() 15 | assert initial_balance - expected_eth_sold == accounts[0].balance() 16 | 17 | 18 | def test_forward_no_value_exact(liquid_lgt, relayer, storage, accounts): 19 | initial_tokens = liquid_lgt.poolTokenReserves() 20 | initial_balance = accounts[0].balance() 21 | calldata = "60fe47b10000000000000000000000000000000000000000000000000000000000000001" 22 | expected_eth_sold = liquid_lgt.getEthToTokenOutputPrice(3) 23 | tx = relayer.forward(3, DEADLINE, storage, 0, calldata, {'from': accounts[0], 'value': expected_eth_sold}) 24 | assert tx.return_value == expected_eth_sold 25 | assert storage.get() == "0x1" 26 | assert initial_tokens - 3 == liquid_lgt.poolTokenReserves() 27 | assert initial_balance - expected_eth_sold == accounts[0].balance() 28 | 29 | 30 | def test_forward_with_value_refund(liquid_lgt, relayer, storage, accounts): 31 | """ send 0.01 ether with the call and buy 2 tokens """ 32 | initial_tokens = liquid_lgt.poolTokenReserves() 33 | initial_balance = accounts[0].balance() 34 | calldata = "bc25bd200000000000000000000000000000000000000000000000000000000000000000" 35 | expected_eth_sold = liquid_lgt.getEthToTokenOutputPrice(2) 36 | tx = relayer.forward(2, DEADLINE, storage, "0.01 ether", calldata, {'from': accounts[0], 'value': "1 ether"}) 37 | assert tx.return_value == expected_eth_sold 38 | assert storage.get() == "0.01 ether" 39 | assert initial_tokens - 2 == liquid_lgt.poolTokenReserves() 40 | assert initial_balance - expected_eth_sold - "0.01 ether" == accounts[0].balance() 41 | 42 | 43 | def test_forward_with_value_exact(liquid_lgt, relayer, storage, accounts): 44 | """ send 0.2 ether with the call and buy 2 tokens """ 45 | initial_tokens = liquid_lgt.poolTokenReserves() 46 | initial_balance = accounts[0].balance() 47 | calldata = "bc25bd200000000000000000000000000000000000000000000000000000000000000000" 48 | expected_eth_sold = liquid_lgt.getEthToTokenOutputPrice(6) 49 | total_ether = Wei(expected_eth_sold + "0.2 ether") 50 | tx = relayer.forward(6, DEADLINE, storage, "0.2 ether", calldata, {'from': accounts[0], 'value': total_ether}) 51 | assert tx.return_value == expected_eth_sold 52 | assert storage.get() == "0.2 ether" 53 | assert initial_tokens - 6 == liquid_lgt.poolTokenReserves() 54 | assert initial_balance - total_ether == accounts[0].balance() 55 | -------------------------------------------------------------------------------- /tests/unit/lgt/test_mint_to_liquidity.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | import brownie 3 | 4 | DEADLINE = 99999999999 5 | 6 | 7 | def test_token_constraint(lgt, accounts): 8 | tx = lgt.mintToLiquidity(4, 0, DEADLINE, accounts[5], {'from': accounts[5], 'value': "0.1 ether"}) 9 | tokens_added, eth_added, liq_added = tx.return_value 10 | assert tokens_added == 4 11 | assert eth_added == Wei("0.004 ether") - 1 12 | assert liq_added == Wei("0.004 ether") - 1 13 | assert lgt.balance() == Wei("0.005 ether") - 1 14 | assert lgt.ownedSupply() == 30 15 | 16 | 17 | def test_eth_constraint(lgt, accounts): 18 | tx = lgt.mintToLiquidity(100, 0, DEADLINE, accounts[5], {'from': accounts[5], 'value': Wei("0.01 ether") - 1}) 19 | tokens_added, eth_added, liq_added = tx.return_value 20 | assert tokens_added == 10 21 | assert eth_added == Wei("0.01 ether") - 1 22 | assert liq_added == Wei("0.01 ether") - 1 23 | assert lgt.balance() == Wei("0.011 ether") - 1 24 | assert lgt.ownedSupply() == 30 25 | assert lgt.poolTotalSupply() == Wei("0.011 ether") - 1 26 | 27 | 28 | def test_exact(lgt, accounts): 29 | tx = lgt.mintToLiquidity(15, 0, DEADLINE, accounts[5], {'from': accounts[5], 'value': Wei("0.015 ether") - 1}) 30 | tokens_added, eth_added, liq_added = tx.return_value 31 | assert tokens_added == 15 32 | assert eth_added == Wei("0.015 ether") - 1 33 | assert liq_added == Wei("0.015 ether") - 1 34 | assert lgt.balance() == Wei("0.016 ether") - 1 35 | assert lgt.ownedSupply() == 30 36 | assert lgt.poolTotalSupply() == Wei("0.016 ether") - 1 37 | 38 | 39 | def test_refund(lgt, accounts): 40 | initial_balance = accounts[5].balance() 41 | tx = lgt.mintToLiquidity(15, 0, DEADLINE, accounts[5], {'from': accounts[5], 'value': Wei("2 ether")}) 42 | tokens_added, eth_added, liq_added = tx.return_value 43 | assert tokens_added == 15 44 | assert eth_added == Wei("0.015 ether") - 1 45 | assert liq_added == Wei("0.015 ether") - 1 46 | assert initial_balance - accounts[5].balance() == eth_added 47 | assert lgt.balance() == Wei("0.016 ether") - 1 48 | assert lgt.ownedSupply() == 30 49 | assert lgt.poolTotalSupply() == Wei("0.016 ether") - 1 50 | 51 | 52 | def test_deadline_reverts(lgt, accounts): 53 | with brownie.reverts("dev: deadline passed"): 54 | lgt.mintToLiquidity(10, 0, 1, accounts[4], {'from': accounts[4], 'value': "0.1 ether"}) 55 | 56 | 57 | def test_min_token_reverts(lgt, accounts): 58 | with brownie.reverts("dev: can't mint less than 1 token"): 59 | lgt.mintToLiquidity(0, 0, DEADLINE, accounts[4], {'from': accounts[4], 'value': "0.1 ether"}) 60 | 61 | 62 | def test_no_eth_reverts(lgt, accounts): 63 | with brownie.reverts("dev: must provide ether to add liquidity"): 64 | lgt.mintToLiquidity(10, 0, DEADLINE, accounts[4], {'from': accounts[4]}) 65 | 66 | 67 | def test_insufficient_liquidity_reverts(lgt, accounts): 68 | with brownie.reverts("dev: not enough liquidity can be created"): 69 | lgt.mintToLiquidity(10, "1 ether", DEADLINE, accounts[4], {'from': accounts[4], 'value': "1 ether"}) 70 | -------------------------------------------------------------------------------- /tests/unit/liquidity/test_remove_liquidity.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import * 3 | import brownie 4 | 5 | 6 | DEADLINE = 9999999999999 7 | 8 | @pytest.fixture() 9 | def liquid_lgt(lgt, accounts): 10 | lgt.addLiquidity(1, 21, 99999999999, {'from': accounts[0], 'value': "0.02 ether"}) 11 | yield lgt 12 | 13 | 14 | def test_remove_liquidity_liq_constraint(liquid_lgt, accounts): 15 | initial_balance = accounts[0].balance() 16 | tx = liquid_lgt.removeLiquidity("0.01 ether", "0.005 ether", 1, DEADLINE, {'from': accounts[0]}) 17 | # should withdraw 0.1 ether and 10 tokens 18 | assert tx.return_value == (Wei("0.01 ether"), 10) 19 | event = tx.events['RemoveLiquidity'] 20 | assert event["provider"] == accounts[0] 21 | assert event["eth_amount"] == "0.01 ether" 22 | assert event["token_amount"] == 10 23 | assert liquid_lgt.balanceOf(accounts[0]) == 30 - 21 + 10 24 | assert accounts[0].balance() == initial_balance + "0.01 ether" 25 | 26 | 27 | def test_remove_liquidity_exact(liquid_lgt, accounts): 28 | initial_balance = accounts[0].balance() 29 | tx = liquid_lgt.removeLiquidity("0.015 ether", "0.015 ether", 15, DEADLINE, {'from': accounts[0]}) 30 | # should withdraw 0.015 ether and 15 tokens 31 | assert tx.return_value == (Wei("0.015 ether"), 15) 32 | assert liquid_lgt.balanceOf(accounts[0]) == 30 - 21 + 15 33 | assert accounts[0].balance() == initial_balance + "0.015 ether" 34 | 35 | 36 | def test_deadline_reverts(liquid_lgt, accounts): 37 | with brownie.reverts("dev: deadline passed"): 38 | liquid_lgt.removeLiquidity("0.01 ether", "0.005 ether", 1, 1, {'from': accounts[0]}) 39 | 40 | 41 | def test_no_min_shares_reverts(liquid_lgt, accounts): 42 | with brownie.reverts("dev: amount of liquidity to remove must be positive"): 43 | liquid_lgt.removeLiquidity("0 ether", "0.005 ether", 1, DEADLINE, {'from': accounts[0]}) 44 | 45 | 46 | def test_no_min_eth_reverts(liquid_lgt, accounts): 47 | with brownie.reverts("dev: must remove positive eth amount"): 48 | liquid_lgt.removeLiquidity("0.01 ether", "0 ether", 1, DEADLINE, {'from': accounts[0]}) 49 | 50 | 51 | def test_no_min_tokens_reverts(liquid_lgt, accounts): 52 | with brownie.reverts("dev: must remove positive token amount"): 53 | liquid_lgt.removeLiquidity("0.01 ether", "0.005 ether", 0, DEADLINE, {'from': accounts[0]}) 54 | 55 | 56 | def test_exceed_eth_reverts(liquid_lgt, accounts): 57 | with brownie.reverts("dev: can't remove enough eth"): 58 | liquid_lgt.removeLiquidity("0.01 ether", "0.015 ether", 1, DEADLINE, {'from': accounts[0]}) 59 | 60 | 61 | def test_exceed_tokens_reverts(liquid_lgt, accounts): 62 | with brownie.reverts("dev: can't remove enough tokens"): 63 | liquid_lgt.removeLiquidity("0.01 ether", "0.005 ether", 20, DEADLINE, {'from': accounts[0]}) 64 | 65 | 66 | def test_too_many_shares_reverts(liquid_lgt, accounts): 67 | """ Trying to remove more shares than owned. """ 68 | with brownie.reverts("SafeMath: subtraction overflow"): 69 | liquid_lgt.removeLiquidity("0.6 ether", "0.005 ether", 1, DEADLINE, {'from': accounts[0]}) 70 | -------------------------------------------------------------------------------- /tests/unit/liquidity/test_transfer_liquidity.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import Wei 4 | 5 | DEADLINE = 9999999999999 6 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 7 | 8 | 9 | @pytest.fixture() 10 | def liquid_lgt(lgt, accounts): 11 | lgt.mint(50, {'from': accounts[0]}) 12 | lgt.addLiquidity(1, 51, 99999999999, {'from': accounts[0], 'value': "0.05 ether"}) 13 | lgt.mint(80, {'from': accounts[1]}) 14 | lgt.addLiquidity(1, 50, 99999999999, {'from': accounts[1], 'value': "0.049 ether"}) 15 | yield lgt 16 | 17 | 18 | def test_transfer_liquidity(liquid_lgt, accounts): 19 | sender, recipient = accounts[:2] 20 | sender_initial_liquidity = liquid_lgt.poolBalanceOf(sender) 21 | recipient_initial_liquidity = liquid_lgt.poolBalanceOf(recipient) 22 | transfer_amount = Wei("0.01 ether") 23 | tx = liquid_lgt.poolTransfer(recipient, transfer_amount, {'from': sender}) 24 | assert tx.return_value 25 | event = tx.events['TransferLiquidity'] 26 | assert event["from"] == sender 27 | assert event["to"] == recipient 28 | assert event["value"] == transfer_amount 29 | assert sender_initial_liquidity - transfer_amount == liquid_lgt.poolBalanceOf(sender) 30 | assert recipient_initial_liquidity + transfer_amount == liquid_lgt.poolBalanceOf(recipient) 31 | 32 | 33 | def test_transfer_liquidity_insufficient_reverts(liquid_lgt, accounts): 34 | sender, recipient = accounts[:2] 35 | sender_initial_liquidity = liquid_lgt.poolBalanceOf(sender) 36 | recipient_initial_liquidity = liquid_lgt.poolBalanceOf(recipient) 37 | transfer_amount = Wei("1 ether") 38 | with brownie.reverts("LGT: transfer exceeds balance"): 39 | liquid_lgt.poolTransfer(recipient, transfer_amount, {'from': sender}) 40 | assert sender_initial_liquidity == liquid_lgt.poolBalanceOf(sender) 41 | assert recipient_initial_liquidity == liquid_lgt.poolBalanceOf(recipient) 42 | 43 | 44 | def test_transfer_liquidity_self_reverts(liquid_lgt, accounts): 45 | sender = accounts[0] 46 | sender_initial_liquidity = liquid_lgt.poolBalanceOf(sender) 47 | recipient_initial_liquidity = liquid_lgt.poolBalanceOf(liquid_lgt) 48 | transfer_amount = Wei("0.001 ether") 49 | with brownie.reverts("dev: can't transfer liquidity to token contract"): 50 | liquid_lgt.poolTransfer(liquid_lgt, transfer_amount, {'from': sender}) 51 | assert sender_initial_liquidity == liquid_lgt.poolBalanceOf(sender) 52 | assert recipient_initial_liquidity == liquid_lgt.poolBalanceOf(liquid_lgt) 53 | 54 | 55 | def test_transfer_liquidity_zero_reverts(liquid_lgt, accounts): 56 | sender = accounts[0] 57 | recipient = ZERO_ADDRESS 58 | sender_initial_liquidity = liquid_lgt.poolBalanceOf(sender) 59 | recipient_initial_liquidity = liquid_lgt.poolBalanceOf(ZERO_ADDRESS) 60 | transfer_amount = Wei("0.005 ether") 61 | with brownie.reverts("dev: can't transfer liquidity to zero address"): 62 | liquid_lgt.poolTransfer(recipient, transfer_amount, {'from': sender}) 63 | assert sender_initial_liquidity == liquid_lgt.poolBalanceOf(sender) 64 | assert recipient_initial_liquidity == liquid_lgt.poolBalanceOf(recipient) 65 | -------------------------------------------------------------------------------- /contracts/LGTRelayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.9; 3 | 4 | import "../interfaces/ILGT.sol"; 5 | import "OpenZeppelin/openzeppelin-contracts@3.0.1/contracts/math/SafeMath.sol"; 6 | 7 | contract LGTRelayer { 8 | using SafeMath for uint256; 9 | ILGT public constant lgt = ILGT(0x000000000000C1CB11D5c062901F32D06248CE48); 10 | 11 | /// @notice Forward a call, buy and free as many tokens as possible for the fixed 12 | /// amount of ether - `value` sent with this transaction. 13 | /// @param deadline The time after which the transaction can no longer be executed. 14 | /// @param destination The address where the call is sent to. 15 | /// @param value The amount of ether to pass with the call. 16 | /// @param data The calldata to send to the `destination`. 17 | /// @return tokensBought The amount of tokens bought and freed. 18 | function forwardMax( 19 | uint256 deadline, 20 | address destination, 21 | uint256 value, 22 | bytes memory data 23 | ) 24 | external 25 | payable 26 | returns (uint256) 27 | { 28 | require(value <= msg.value); 29 | uint256 tokensBought = lgt.buyMaxAndFree{value : msg.value - value}(deadline); 30 | destination.call{value : value}(data); 31 | return tokensBought; 32 | } 33 | 34 | /// @notice Forward a call, buy and free `tokenAmount` of tokens. 35 | /// Any remaining ether is refunded. 36 | /// If not enough ether is sent, no purchase happens and everything is refunded. 37 | /// @param tokenAmount The amount of tokens to buy and free. 38 | /// @param deadline The time after which the transaction can no longer be executed. 39 | /// @param destination The address where the call is sent to. 40 | /// @param value The amount of ether to pass with the call. 41 | /// @param data The calldata to send to the `destination`. 42 | /// @return ethSold The amount of ether spent to buy the tokens, the rest is refunded. 43 | function forward( 44 | uint256 tokenAmount, 45 | uint256 deadline, 46 | address destination, 47 | uint256 value, 48 | bytes memory data 49 | ) 50 | external 51 | payable 52 | returns (uint256) 53 | { 54 | require(value <= msg.value); 55 | uint256 ethSold = lgt.buyAndFree{value : msg.value - value}(tokenAmount, deadline, msg.sender); 56 | destination.call{value : value}(data); 57 | return ethSold; 58 | } 59 | 60 | /// @notice Calculate the optimal amount of tokens and buy them if costs can be reduced. 61 | /// Otherwise, execute the transaction without buying tokens. 62 | /// @dev optimal tokens = (gas_cost + overhead) // 41300 63 | /// overhead = ~55000 64 | function forwardAuto( 65 | address destination, 66 | uint256 value, 67 | bytes memory data 68 | ) 69 | external 70 | payable 71 | returns (uint256 optimalTokens, uint256 buyCost) 72 | { 73 | uint256 initialGas = gasleft(); 74 | require(value <= msg.value); 75 | destination.call{value : value}(data); 76 | optimalTokens = (initialGas - gasleft() + 55000) / 41300; 77 | if (optimalTokens > 0) { 78 | buyCost = lgt.getEthToTokenOutputPrice(optimalTokens); 79 | if (buyCost < ((18145 * optimalTokens) - 24000) * tx.gasprice) { 80 | lgt.buyAndFree{value : msg.value - value}(optimalTokens, now, msg.sender); 81 | } else { 82 | msg.sender.call{value : msg.value - value}(""); 83 | } 84 | } else { 85 | msg.sender.call{value : msg.value - value}(""); 86 | } 87 | return (optimalTokens, buyCost); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/unit/lgt/test_mint_to_sell.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import brownie 3 | 4 | DEADLINE = 99999999999 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def liquid_lgt(lgt, accounts): 9 | lgt.addLiquidity(1, 20, DEADLINE, {'from': accounts[0], 'value': "0.019 ether"}) 10 | yield lgt 11 | 12 | 13 | def test_mint_to_sell(liquid_lgt, accounts): 14 | initial_balance = accounts[4].balance() 15 | expected_eth_payout = liquid_lgt.getTokenToEthInputPrice(10) 16 | tx = liquid_lgt.mintToSell(10, 0, DEADLINE, {'from': accounts[4]}) 17 | assert tx.return_value == expected_eth_payout 18 | assert accounts[4].balance() - initial_balance == expected_eth_payout 19 | assert liquid_lgt.ownedSupply() == 10 20 | assert liquid_lgt.poolTokenReserves() == 21 + 10 21 | 22 | 23 | def test_mint_to_sell_opt(liquid_lgt, accounts): 24 | initial_owned_supply = liquid_lgt.ownedSupply() 25 | initial_balance = accounts[4].balance() 26 | expected_eth_payout = liquid_lgt.getTokenToEthInputPrice(20) 27 | liquid_lgt.mintToSell9630191(20, {'from': accounts[4]}) 28 | assert accounts[4].balance() - initial_balance == expected_eth_payout 29 | assert liquid_lgt.ownedSupply() == initial_owned_supply 30 | assert liquid_lgt.poolTokenReserves() == 21 + 20 31 | 32 | 33 | def test_mint_to_sell_to(liquid_lgt, accounts): 34 | initial_balance4 = accounts[4].balance() 35 | initial_balance5 = accounts[5].balance() 36 | expected_eth_payout = liquid_lgt.getTokenToEthInputPrice(20) 37 | tx = liquid_lgt.mintToSellTo(20, 0, DEADLINE, accounts[5], {'from': accounts[4]}) 38 | assert tx.return_value == expected_eth_payout 39 | assert accounts[4].balance() == initial_balance4 40 | assert accounts[5].balance() - initial_balance5 == expected_eth_payout 41 | assert liquid_lgt.ownedSupply() == 10 42 | assert liquid_lgt.poolTokenReserves() == 21 + 20 43 | 44 | 45 | def test_mint_to_sell_to_opt(liquid_lgt, accounts): 46 | initial_balance4 = accounts[4].balance() 47 | initial_balance5 = accounts[5].balance() 48 | expected_eth_payout = liquid_lgt.getTokenToEthInputPrice(30) 49 | liquid_lgt.mintToSellTo25630722(30, accounts[5], {'from': accounts[4]}) 50 | assert accounts[4].balance() == initial_balance4 51 | assert accounts[5].balance() - initial_balance5 == expected_eth_payout 52 | assert liquid_lgt.ownedSupply() == 10 53 | assert liquid_lgt.poolTokenReserves() == 21 + 30 54 | 55 | 56 | def test_deadline_reverts(liquid_lgt, accounts): 57 | with brownie.reverts("dev: deadline passed"): 58 | liquid_lgt.mintToSell(10, 0, 1, {'from': accounts[4]}) 59 | 60 | 61 | def test_no_tokens_sell_reverts(liquid_lgt, accounts): 62 | with brownie.reverts("dev: must sell one or more tokens"): 63 | liquid_lgt.mintToSell(0, 0, DEADLINE, {'from': accounts[4]}) 64 | 65 | 66 | def test_insufficient_payout_reverts(liquid_lgt, accounts): 67 | with brownie.reverts("dev: tokens not worth enough"): 68 | eth_bought = liquid_lgt.getTokenToEthInputPrice(10) 69 | liquid_lgt.mintToSell(10, eth_bought + 1, DEADLINE, {'from': accounts[4]}) 70 | 71 | 72 | def test_to_deadline_reverts(liquid_lgt, accounts): 73 | with brownie.reverts("dev: deadline passed"): 74 | liquid_lgt.mintToSellTo(10, 0, 1, accounts[5], {'from': accounts[4]}) 75 | 76 | 77 | def test_to_no_tokens_sell_reverts(liquid_lgt, accounts): 78 | with brownie.reverts("dev: must sell one or more tokens"): 79 | liquid_lgt.mintToSellTo(0, 0, DEADLINE, accounts[5], {'from': accounts[4]}) 80 | 81 | 82 | def test_to_insufficient_payout_reverts(liquid_lgt, accounts): 83 | with brownie.reverts("dev: tokens not worth enough"): 84 | eth_bought = liquid_lgt.getTokenToEthInputPrice(10) 85 | liquid_lgt.mintToSellTo(10, eth_bought + 1, DEADLINE, accounts[5], {'from': accounts[4]}) -------------------------------------------------------------------------------- /tests/unit/lgt/test_deploy.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | DEADLINE = 99999999999 5 | storage_bytecode = "0x6080604052600560005534801561001557600080fd5b5060ac806100246000" \ 6 | "396000f3fe6080604052348015600f57600080fd5b5060043610603257600035" \ 7 | "60e01c806360fe47b11460375780636d4ce63c146053575b600080fd5b605160" \ 8 | "048036036020811015604b57600080fd5b5035606b565b005b60596070565b60" \ 9 | "408051918252519081900360200190f35b600055565b6000549056fea2646970" \ 10 | "667358221220da99a6a9d4cea3f86897beaabcc36a956a9de39ec09abb36fa08" \ 11 | "6b5e25243df164736f6c63430006070033" 12 | storage_abi = [ 13 | { 14 | "inputs": [], 15 | "name": "get", 16 | "outputs": [ 17 | { 18 | "internalType": "uint256", 19 | "name": "", 20 | "type": "uint256" 21 | } 22 | ], 23 | "stateMutability": "view", 24 | "type": "function" 25 | }, 26 | { 27 | "inputs": [ 28 | { 29 | "internalType": "uint256", 30 | "name": "_x", 31 | "type": "uint256" 32 | } 33 | ], 34 | "name": "set", 35 | "outputs": [], 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | } 39 | ] 40 | 41 | 42 | @pytest.fixture(scope="module") 43 | def liquid_lgt(lgt, accounts): 44 | lgt.mint(50, {'from': accounts[0]}) 45 | lgt.addLiquidity(1, 51, 99999999999, {'from': accounts[0], 'value': "0.05 ether"}) 46 | lgt.mint(80, {'from': accounts[1]}) 47 | lgt.addLiquidity(1, 50, 99999999999, {'from': accounts[1], 'value': "0.049 ether"}) 48 | yield lgt 49 | 50 | 51 | def test_deploy(liquid_lgt, accounts, Contract): 52 | initial_tokens = liquid_lgt.poolTokenReserves() 53 | price = liquid_lgt.getEthToTokenOutputPrice(5) 54 | tx = liquid_lgt.deploy(5, DEADLINE, storage_bytecode, {'from': accounts[0], 'value': price}) 55 | address = tx.return_value 56 | contract = Contract.from_abi(name="Storage", address=address, abi=storage_abi, owner=accounts[0]) 57 | assert contract.get() == 5 58 | contract.set(10) 59 | assert contract.get() == 10 60 | assert initial_tokens - 5 == liquid_lgt.poolTokenReserves() 61 | 62 | 63 | def test_deploy_refund(liquid_lgt, accounts): 64 | initial_balance = accounts[0].balance() 65 | price = liquid_lgt.getEthToTokenOutputPrice(5) 66 | liquid_lgt.deploy(5, DEADLINE, storage_bytecode, {'from': accounts[0], 'value': price * 2}) 67 | assert initial_balance - price == accounts[0].balance() 68 | 69 | 70 | def test_deploy_deadline_reverts(liquid_lgt, accounts): 71 | with brownie.reverts("dev: deadline passed"): 72 | liquid_lgt.deploy(5, 1, storage_bytecode, {'from': accounts[0], 'value': "1 ether"}) 73 | 74 | 75 | def test_create2(liquid_lgt, accounts, Contract): 76 | initial_tokens = liquid_lgt.poolTokenReserves() 77 | price = liquid_lgt.getEthToTokenOutputPrice(4) 78 | tx = liquid_lgt.create2(4, DEADLINE, "0xabc", storage_bytecode, {'from': accounts[0], 'value': price}) 79 | address = tx.return_value 80 | contract = Contract.from_abi(name="Storage", address=address, abi=storage_abi, owner=accounts[0]) 81 | assert contract.get() == 5 82 | contract.set(10) 83 | assert contract.get() == 10 84 | assert initial_tokens - 4 == liquid_lgt.poolTokenReserves() 85 | 86 | 87 | def test_create2_refund(liquid_lgt, accounts): 88 | initial_balance = accounts[0].balance() 89 | price = liquid_lgt.getEthToTokenOutputPrice(3) 90 | liquid_lgt.create2(3, DEADLINE, "0xabc", storage_bytecode, {'from': accounts[0], 'value': price * 2}) 91 | assert initial_balance - price == accounts[0].balance() 92 | 93 | 94 | def test_create2_deadline_reverts(liquid_lgt, accounts): 95 | with brownie.reverts("dev: deadline passed"): 96 | liquid_lgt.create2(4, 1, "0xabc", storage_bytecode, {'from': accounts[0], 'value': "1 ether"}) 97 | -------------------------------------------------------------------------------- /scripts/benchmarks/gas_token_comparison.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | 3 | from brownie.utils import color 4 | from scripts.all_gts import main as get_all_gas_tokens 5 | 6 | TOKENS_MINT = 25 7 | TOKENS_FREE = 25 8 | BURN = 1000000 9 | DEADLINE = 999999999999 10 | 11 | 12 | def main(): 13 | rpc.reset() 14 | # get gas token contracts 15 | lgt, gst, chi, h, me = get_all_gas_tokens() 16 | 17 | # get uniswap exchanges (and fund them) 18 | gst_uniswap = interface.UniswapExchangeInterface("0x929507CD3D90Ab11eC4822E9eB5A48eb3a178F19") 19 | gst.approve(gst_uniswap, 2 ** 256 - 1, {'from': accounts[0]}) 20 | chi.mint(150, {'from': accounts[9]}) 21 | chi_uniswap = interface.UniswapExchangeInterface("0xD772f5ac5c4145f3B2b460515d277f667253E6Dc") 22 | chi.approve(chi_uniswap, 2 ** 256 - 1, {'from': accounts[0]}) 23 | chi.approve(chi_uniswap, 2 ** 256 - 1, {'from': accounts[9]}) 24 | chi_uniswap.addLiquidity(1, 150, 99999999999, {'from': accounts[9], 'value': "0.04 ether"}) 25 | rpc.snapshot() 26 | out = "Comparing Liquid Gas Token, CHI and GST2...\n" 27 | 28 | # Minting 29 | out += f"\n Minting {TOKENS_MINT} tokens:\n" 30 | 31 | tx = gst.mint(TOKENS_MINT, {'from': accounts[0]}) 32 | out += f" GST2: {str(tx.gas_used).ljust(6)} gas\n" 33 | tx = chi.mint(TOKENS_MINT, {'from': accounts[0]}) 34 | out += f" {color('dark green')}CHI: {str(tx.gas_used).ljust(6)} gas{color}\n" 35 | tx = lgt.mint(TOKENS_MINT, {'from': accounts[0]}) 36 | out += f" LGT: {str(tx.gas_used).ljust(6)} gas\n" 37 | rpc.revert() 38 | 39 | # Minting and selling 40 | out += f"\n Minting {TOKENS_MINT} tokens and selling them on Uniswap:\n" 41 | 42 | tx_mint = gst.mint(TOKENS_MINT, {'from': accounts[0]}) 43 | tx_sell = gst_uniswap.tokenToEthSwapInput(TOKENS_MINT, 1, 9999999999, {'from': accounts[0]}) 44 | out += f" GST2: {str(tx_mint.gas_used + tx_sell.gas_used).ljust(6)} gas\n" 45 | 46 | tx_mint = chi.mint(TOKENS_MINT, {'from': accounts[0]}) 47 | tx_sell = chi_uniswap.tokenToEthSwapInput(TOKENS_MINT, 1, 9999999999, {'from': accounts[0]}) 48 | out += f" CHI: {str(tx_mint.gas_used + tx_sell.gas_used).ljust(6)} gas\n" 49 | 50 | tx = lgt.mintToSell9630191(TOKENS_MINT, {'from': accounts[0]}) 51 | out += f" {color('dark green')}LGT: {str(tx.gas_used).ljust(6)} gas{color}\n" 52 | rpc.revert() 53 | 54 | # Burn Gas and Free 55 | out += f"\n Burning {BURN} gas and freeing {TOKENS_FREE} tokens:\n" 56 | 57 | tx = h.burnAndFreeGST(BURN, TOKENS_FREE, {'from': accounts[0]}) 58 | out += f" GST2: {str(tx.gas_used).ljust(6)} gas\n" 59 | tx = h.burnAndFreeCHI(BURN, TOKENS_FREE, {'from': accounts[0]}) 60 | out += f" CHI: {str(tx.gas_used).ljust(6)} gas\n" 61 | tx = h.burnAndFree(BURN, TOKENS_FREE, {'from': accounts[0]}) 62 | out += f" {color('dark green')}LGT: {str(tx.gas_used).ljust(6)} gas{color}\n" 63 | rpc.revert() 64 | 65 | # Buy Tokens Burn Gas And Free 66 | out += f"\n Buying {TOKENS_FREE} tokens on Uniswap, then burning {BURN} gas and freeing the tokens:\n" 67 | 68 | price = gst_uniswap.getEthToTokenOutputPrice(TOKENS_FREE) 69 | tx_buy = gst_uniswap.ethToTokenSwapOutput(TOKENS_FREE, 9999999999, {'from': accounts[0], 'value': price}) 70 | tx_free = h.burnAndFreeGST(BURN, TOKENS_FREE, {'from': accounts[0]}) 71 | out += f" GST2: {str(tx_buy.gas_used + tx_free.gas_used).ljust(6)} gas\n" 72 | 73 | price = chi_uniswap.getEthToTokenOutputPrice(TOKENS_FREE) 74 | tx_buy = chi_uniswap.ethToTokenSwapOutput(TOKENS_FREE, 9999999999, {'from': accounts[0], 'value': price}) 75 | tx_free = h.burnAndFreeCHI(BURN, TOKENS_FREE, {'from': accounts[0]}) 76 | out += f" CHI: {str(tx_buy.gas_used + tx_free.gas_used).ljust(6)} gas\n" 77 | 78 | price = lgt.getEthToTokenOutputPrice(TOKENS_FREE) 79 | tx = h.burnBuyAndFreeOpt(BURN, TOKENS_FREE, {'from': accounts[0], 'value': price}) 80 | out += f" {color('dark green')}LGT: {str(tx.gas_used).ljust(6)} gas{color}\n" 81 | rpc.revert() 82 | 83 | print(out) 84 | network.disconnect() 85 | -------------------------------------------------------------------------------- /scripts/benchmarks/gas_benchmarks.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from brownie import * 5 | from brownie.utils import color 6 | 7 | DEADLINE = 99999999999 8 | TEST_SET = [1, 15, 32, 71] 9 | BENCHMARK_FILE = Path(__file__).parent.absolute().joinpath(Path("benchmarks.json")) 10 | 11 | 12 | class MissedBenchmark(Exception): 13 | pass 14 | 15 | 16 | def color_string(string, col): 17 | return f"{color(col)}{string}{color}" 18 | 19 | 20 | def main(update_benchmarks: str = "never"): 21 | # Set up LGT contract 22 | rpc.reset() 23 | with BENCHMARK_FILE.open() as fp: 24 | benchmarks = json.load(fp) 25 | 26 | lgt_deployer = accounts.add("0x7d4cbcfd42fe584226a17f385f734b046090f3e9d9fd95b2e10ef53acbbc39e2") 27 | 28 | accounts[9].transfer("0x000000000049091f98692b2460500b6d133ae31f", "0.001 ether") 29 | lgt = lgt_deployer.deploy(LiquidGasToken) 30 | lgt.mint(80, {'from': accounts[0]}) 31 | lgt.addLiquidity(1, 50, DEADLINE, {'from': accounts[0], 'value': "0.049 ether"}) 32 | lgt.mint(80, {'from': accounts[1]}) 33 | lgt.addLiquidity(1, 50, DEADLINE, {'from': accounts[1], 'value': "0.049 ether"}) 34 | 35 | rpc.snapshot() 36 | 37 | # Run benchmarks 38 | results = benchmarks.copy() 39 | for category in benchmarks: 40 | for function in benchmarks[category]: 41 | gas_used = [] 42 | for tokens in TEST_SET: 43 | rpc.revert() 44 | args = [ 45 | accounts[0] if arg == 'account' else arg 46 | for arg in benchmarks[category][function]["args"] 47 | ] 48 | tx_args = {'from': accounts[0]} 49 | if "value" in benchmarks[category][function]: 50 | try: 51 | tx_args["value"] = Wei(benchmarks[category][function]["value"]) 52 | except TypeError: 53 | tx_args["value"] = getattr( 54 | lgt, 55 | benchmarks[category][function]["value"] 56 | )(tokens) 57 | tx = getattr(lgt, function)(tokens, *args, tx_args) 58 | gas_used.append(tx.gas_used) 59 | results[category][function]["gas_used"] = gas_used 60 | 61 | # Process results 62 | total_improvement = 0 63 | for category in benchmarks: 64 | for function in benchmarks[category]: 65 | mark = benchmarks[category][function]["gas_used"] 66 | score = results[category][function]["gas_used"] 67 | for i in range(len(TEST_SET)): 68 | improvement = mark[i] - score[i] 69 | total_improvement += improvement 70 | out_string = f"{color_string(function, 'bright magenta')}({TEST_SET[i]}) " \ 71 | f"{str(score[i]).ljust(7)} gas used: " 72 | out_string = out_string.rjust(60) 73 | if improvement > 0: 74 | out_string += color_string(f"[ BENCHMARK IMPROVED BY {improvement} ]", "dark green") 75 | elif improvement < 0: 76 | out_string += color_string(f"[ BENCHMARK MISSED BY {improvement} ]", "dark red") 77 | else: 78 | out_string += color_string("[ BENCHMARK MATCHED ]", "bright yellow") 79 | print(out_string) 80 | 81 | ti_col = "bright yellow" 82 | if total_improvement > 0: 83 | ti_col = "dark green" 84 | elif total_improvement < 0: 85 | ti_col = "dark red" 86 | print(f"\n TOTAL IMPROVEMENT: {color_string(total_improvement, ti_col)}") 87 | 88 | # Update Benchmarks 89 | if ( 90 | update_benchmarks == "always" 91 | or (update_benchmarks == "on_improvement" and total_improvement > 0) 92 | ): 93 | print("\n Updating Benchmarks...") 94 | with BENCHMARK_FILE.open("w") as fp: 95 | json.dump(results, fp, sort_keys=True, indent=2, default=sorted) 96 | 97 | network.disconnect() 98 | # Raise if benchmarks missed 99 | if total_improvement < 0: 100 | raise MissedBenchmark(f"Benchmark missed by {total_improvement}") 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Liquid Gas Token 2 | 3 | Master Thesis Project of **Matthias Nadler**, University of Basel 4 | 5 | Supervised by **Prof. Dr. Fabian Schär**, Credit Suisse Asset Management (Schweiz) Professor for Distributed Ledger Technologies and 6 | Fintech Center for Innovative Finance, University of Basel 7 | 8 | [Read the full thesis, including the LGT Whitepaper.](thesis/LiquidGasToken_Nadler2020.pdf) 9 | 10 | ## Liquid Gas Token Exchange dApp 11 | 12 | Visit [lgt.exchange](https://lgt.exchange) to start saving gas on your transactions and deployments today! 13 | 14 | ## Introduction 15 | 16 | The Liquid Gas Token (LGT) combines an ERC20 Gas Token (as popularized by [gastoken.io](https://gastoken.io/) and later 17 | [1inch.exchange's CHI](https://github.com/CryptoManiacsZone/chi)) 18 | with an internal liquidity pool, very similar to [Uniswap V1](https://github.com/Uniswap/uniswap-v1). 19 | 20 | The LGT's liquidity pool token reserve is implicitly defined as the amount of *"unowned"* tokens. 21 | This allows for very efficient minting and freeing of tokens when combined with ownership transfers. 22 | 23 | In addition to the usual mint and free functions LGT offers functions to very efficiently do multiple actions in one transaction: 24 | 25 | * mint and sell 26 | * mint and add to liquidity 27 | * buy and free 28 | 29 | The full interface can be seen here: [`ILGT`](interfaces/ILGT.sol), [`ILiquidERC20`](interfaces/ILiquidERC20.sol), [`IERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) 30 | 31 | Gas price arbitrage can very easily be realized by minting LGT and directly receiving ether in return in the same transaction. This function is open to every user (and bot ;-)). The amount of received ether can be verified before sending the transaction, giving the minter full control and guaranteeing the arbitrage profit will be fully realised. 32 | 33 | ## Benchmarks 34 | 35 | Comparative benchmarks for GST2, CHI and LGT. For GST2 and CHI, uniswap is used to do the buying and selling, LGT uses its own liquidity pool. 36 | 37 | This does not give a full representation of gas costs, just a snapshot with the used token amounts. Care has been taken that all variables are initialized and the comparison is as fair as possible. 38 | 39 | Benchmarks were calculated using a [Brownie](https://github.com/eth-brownie/brownie) script on a [Ganache](https://github.com/trufflesuite/ganache-cli) forked main net. You can see the script and details [here](scripts/benchmarks/gas_token_comparison.py). 40 | 41 | ### Minting 4 tokens 42 | ``` 43 | GST2: 179564 gas (mint to balance) 44 | CHI: 180463 gas (mint to balance) 45 | LGT: 185045 gas (mint to balance) 46 | ``` 47 | 48 | ### Minting 4 tokens and selling them on Uniswap 49 | ``` 50 | GST2: 245360 gas (mint and sell) 51 | CHI: 246548 gas (mint and sell) 52 | LGT: 183214 gas (mint and sell) 53 | ``` 54 | 55 | ### Burning 1000000 gas and freeing 25 tokens 56 | ``` 57 | GST2: 609178 gas (free from owned) 58 | CHI: 599767 gas (free from owned) 59 | LGT: 598055 gas (free from owned) 60 | ``` 61 | 62 | ### Buying 25 tokens on Uniswap, then burning 1000000 gas and freeing the tokens 63 | ``` 64 | GST2: 661449 gas (buy and free) 65 | CHI: 651093 gas (buy and free) 66 | LGT: 592585 gas (buy and free) 67 | ``` 68 | 69 | LGT is optimized to buy or sell tokens in the same transaction they are freed or minted. 70 | In these metrics it vastly outperforms the alternatives. 71 | 72 | ## Testing 73 | 74 | To run tests, clone the repo and use: 75 | 76 | ```bash 77 | brownie test tests/unit/ -C -n auto 78 | brownie test tests/integration/ -n auto --network mainnet-fork 79 | ``` 80 | 81 | To run the benchmark script: 82 | 83 | ```bash 84 | brownie run benchmarks/gas_token_comparison --network mainnet-fork 85 | ``` 86 | 87 | ## Project Status 88 | 89 | The LGT smart contract is deployed on the Ethereum Main Net, Kovan and Ropsten at the address: [0x000000000000C1CB11D5c062901F32D06248CE48](https://etherscan.io/address/0x000000000000c1cb11d5c062901f32d06248ce48). 90 | 91 | **This repository has not undergone a formal audit**. Comments, questions, criticisms and pull requests are welcomed. Feel free to reach out on [Gitter](https://gitter.im/matnad). 92 | 93 | 94 | ## License 95 | 96 | This project is licensed under the [MIT license](LICENSE). 97 | -------------------------------------------------------------------------------- /interfaces/UniswapExchangeInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.7; 2 | 3 | interface UniswapExchangeInterface { 4 | // Address of ERC20 token sold on this exchange 5 | function tokenAddress() external view returns (address token); 6 | // Address of Uniswap Factory 7 | function factoryAddress() external view returns (address factory); 8 | // Provide Liquidity 9 | function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256); 10 | function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) external returns (uint256, uint256); 11 | // Get Prices 12 | function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought); 13 | function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold); 14 | function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought); 15 | function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold); 16 | // Trade ETH to ERC20 17 | function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought); 18 | function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address recipient) external payable returns (uint256 tokens_bought); 19 | function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold); 20 | function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address recipient) external payable returns (uint256 eth_sold); 21 | // Trade ERC20 to ETH 22 | function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought); 23 | function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline, address recipient) external returns (uint256 eth_bought); 24 | function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold); 25 | function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold); 26 | // Trade ERC20 to ERC20 27 | function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external returns (uint256 tokens_bought); 28 | function tokenToTokenTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_bought); 29 | function tokenToTokenSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address token_addr) external returns (uint256 tokens_sold); 30 | function tokenToTokenTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_sold); 31 | // Trade ERC20 to Custom Pool 32 | function tokenToExchangeSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address exchange_addr) external returns (uint256 tokens_bought); 33 | function tokenToExchangeTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address exchange_addr) external returns (uint256 tokens_bought); 34 | function tokenToExchangeSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address exchange_addr) external returns (uint256 tokens_sold); 35 | function tokenToExchangeTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address exchange_addr) external returns (uint256 tokens_sold); 36 | // ERC20 comaptibility for liquidity tokens 37 | // bytes32 public name; 38 | // bytes32 public symbol; 39 | // uint256 public decimals; 40 | function transfer(address _to, uint256 _value) external returns (bool); 41 | function transferFrom(address _from, address _to, uint256 value) external returns (bool); 42 | function approve(address _spender, uint256 _value) external returns (bool); 43 | function allowance(address _owner, address _spender) external view returns (uint256); 44 | function balanceOf(address _owner) external view returns (uint256); 45 | function totalSupply() external view returns (uint256); 46 | // Never use 47 | function setup(address token_addr) external; 48 | } -------------------------------------------------------------------------------- /tests/unit/lgt/test_buy_and_free.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import * 4 | 5 | """ 6 | These tests don't verify if gas is refunded, only if tokens are bought and burned. 7 | Refund verification is done with integration tests. 8 | """ 9 | 10 | DEADLINE = 99999999999 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def liquid_lgt(lgt, accounts): 15 | lgt.addLiquidity(1, 20, DEADLINE, {'from': accounts[0], 'value': "0.019 ether"}) 16 | yield lgt 17 | 18 | 19 | def test_buy_and_free(liquid_lgt, accounts): 20 | initial_supply = liquid_lgt.totalSupply() 21 | expected_price = liquid_lgt.getEthToTokenOutputPrice(5) 22 | tx = liquid_lgt.buyAndFree(5, DEADLINE, accounts[1], {'from': accounts[1], 'value': "1 ether"}) 23 | assert tx.return_value == expected_price 24 | assert liquid_lgt.totalSupply() == initial_supply - 5 25 | 26 | 27 | def test_buy_and_free_zero(liquid_lgt, accounts): 28 | """ The price for 0 tokens is 1 wei. """ 29 | initial_balance = accounts[1].balance() 30 | initial_supply = liquid_lgt.totalSupply() 31 | expected_price = liquid_lgt.getEthToTokenOutputPrice(0) 32 | tx = liquid_lgt.buyAndFree(0, DEADLINE, accounts[1], {'from': accounts[1], 'value': "1 ether"}) 33 | assert tx.return_value == expected_price 34 | assert liquid_lgt.totalSupply() == initial_supply 35 | assert initial_balance == accounts[1].balance() + expected_price 36 | 37 | 38 | def test_buy_and_free_exact(liquid_lgt, accounts): 39 | initial_supply = liquid_lgt.totalSupply() 40 | expected_price = liquid_lgt.getEthToTokenOutputPrice(5) 41 | tx = liquid_lgt.buyAndFree(5, DEADLINE, accounts[1], {'from': accounts[1], 'value': expected_price}) 42 | assert tx.return_value == expected_price 43 | assert initial_supply == liquid_lgt.totalSupply() + 5 44 | 45 | 46 | def test_buy_and_free_opt(liquid_lgt, accounts): 47 | initial_supply = liquid_lgt.totalSupply() 48 | expected_price = liquid_lgt.getEthToTokenOutputPrice(1) 49 | liquid_lgt.buyAndFree22457070633(1, {'from': accounts[1], 'value': expected_price}) 50 | assert initial_supply == liquid_lgt.totalSupply() + 1 51 | 52 | 53 | def test_opt_not_enough_eth_sent(liquid_lgt, accounts): 54 | """ No refunds and checks, but eth sent must be sufficient. """ 55 | initial_balance = accounts[1].balance() 56 | initial_supply = liquid_lgt.poolTokenReserves() 57 | expected_price = liquid_lgt.getEthToTokenOutputPrice(5) 58 | assert expected_price > 0 59 | liquid_lgt.buyAndFree22457070633(5, {'from': accounts[1], 'value': expected_price / 2}) 60 | assert initial_supply == liquid_lgt.poolTokenReserves() 61 | assert initial_balance == accounts[1].balance() + expected_price / 2 62 | 63 | 64 | def test_deadline_fails(liquid_lgt, accounts): 65 | initial_balance = accounts[1].balance() 66 | initial_token_reserves = liquid_lgt.poolTokenReserves() 67 | tx = liquid_lgt.buyAndFree(5, 1, accounts[1], {'from': accounts[1], 'value': "1 ether"}) 68 | assert tx.return_value == 0 69 | assert liquid_lgt.poolTokenReserves() == initial_token_reserves 70 | assert initial_balance == accounts[1].balance() 71 | 72 | 73 | def test_exceed_reserve_fails(liquid_lgt, accounts): 74 | initial_balance = accounts[1].balance() 75 | initial_token_reserves = liquid_lgt.poolTokenReserves() 76 | tx = liquid_lgt.buyAndFree(50, DEADLINE, accounts[1], {'from': accounts[1], 'value': "1 ether"}) 77 | assert tx.return_value == 0 78 | assert liquid_lgt.poolTokenReserves() == initial_token_reserves 79 | assert initial_balance == accounts[1].balance() 80 | 81 | 82 | def test_not_enough_eth_sent_fails(liquid_lgt, accounts): 83 | initial_balance = accounts[1].balance() 84 | initial_token_reserves = liquid_lgt.poolTokenReserves() 85 | expected_price = liquid_lgt.getEthToTokenOutputPrice(10) 86 | assert expected_price > "0.01 ether" 87 | tx = liquid_lgt.buyAndFree(10, DEADLINE, accounts[1], {'from': accounts[1], 'value': "0.01 ether"}) 88 | assert tx.return_value == 0 89 | assert liquid_lgt.poolTokenReserves() == initial_token_reserves 90 | assert initial_balance == accounts[1].balance() 91 | 92 | 93 | def test_no_eth_sent_fails(liquid_lgt, accounts): 94 | initial_balance = accounts[1].balance() 95 | initial_token_reserves = liquid_lgt.poolTokenReserves() 96 | tx = liquid_lgt.buyAndFree(10, DEADLINE, accounts[1], {'from': accounts[1]}) 97 | assert tx.return_value == 0 98 | assert liquid_lgt.poolTokenReserves() == initial_token_reserves 99 | assert initial_balance == accounts[1].balance() 100 | 101 | 102 | def test_buy_max_and_free(liquid_lgt, accounts): 103 | expected_tokens = liquid_lgt.getEthToTokenInputPrice(Wei("0.05 ether")) 104 | assert expected_tokens < 50 105 | tx = liquid_lgt.buyMaxAndFree(DEADLINE, {'from': accounts[1], 'value': "0.05 ether"}) 106 | assert tx.return_value == expected_tokens 107 | 108 | 109 | def test_max_deadline_reverts(liquid_lgt, accounts): 110 | initial_token_reserves = liquid_lgt.poolTokenReserves() 111 | with brownie.reverts("dev: deadline passed"): 112 | liquid_lgt.buyMaxAndFree(1, {'from': accounts[1], 'value': "1 ether"}) 113 | assert liquid_lgt.poolTokenReserves() == initial_token_reserves 114 | 115 | 116 | def test_max_no_eth_fails(liquid_lgt, accounts): 117 | initial_token_reserves = liquid_lgt.poolTokenReserves() 118 | tx = liquid_lgt.buyMaxAndFree(DEADLINE, {'from': accounts[1]}) 119 | assert tx.return_value == 0 120 | assert liquid_lgt.poolTokenReserves() == initial_token_reserves 121 | -------------------------------------------------------------------------------- /tests/unit/liquidity/test_sell_lgt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | import pytest 4 | from brownie import * 5 | from brownie.test import given 6 | from hypothesis import settings, strategies as st 7 | 8 | DEADLINE = 99999999999 9 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 10 | FEE_MODIFIER = 995 11 | mint = 60 12 | TOKEN_RESERVE = 70 13 | ETH_RESERVE = Wei("0.069 ether") 14 | 15 | account5_supply = 80 16 | 17 | 18 | def token_to_eth_output(token_amount_to_sell: int) -> "Wei": 19 | """ Constant Price Model implementation with fee. """ 20 | input_amount_with_fee = token_amount_to_sell * FEE_MODIFIER 21 | numerator = input_amount_with_fee * ETH_RESERVE 22 | denominator = TOKEN_RESERVE * 1000 + input_amount_with_fee 23 | return Wei(numerator // denominator) 24 | 25 | 26 | def token_to_eth_input(eth_amount_to_buy: int) -> "Wei": 27 | """ Constant Price Model implementation with fee. """ 28 | numerator = TOKEN_RESERVE * eth_amount_to_buy * 1000 29 | denominator = (ETH_RESERVE - eth_amount_to_buy) * FEE_MODIFIER 30 | return Wei(numerator // denominator + 1) 31 | 32 | 33 | st_sell_amount_token = st.integers(min_value=1, max_value=account5_supply) 34 | st_buy_amount_eth = st.integers( 35 | min_value=int(Wei("1 gwei")), 36 | max_value=int(token_to_eth_output(account5_supply)) 37 | ) 38 | 39 | 40 | @pytest.fixture(scope="module") 41 | def liquidLgt(lgt, accounts): 42 | lgt.mint(mint, {'from': accounts[0]}) 43 | lgt.mint(account5_supply, {'from': accounts[5]}) 44 | lgt.addLiquidity(1, TOKEN_RESERVE - 1, DEADLINE, {'from': accounts[0], 'value': ETH_RESERVE - "0.001 ether"}) 45 | yield lgt 46 | 47 | 48 | @given(sell_amount=st_sell_amount_token) 49 | @settings(max_examples=20) 50 | def test_sell_lgt_input(liquidLgt, accounts, sell_amount): 51 | initial_balance = accounts[5].balance() 52 | initial_reserve = liquidLgt.balance() 53 | 54 | eth_bought = token_to_eth_output(sell_amount) 55 | tx = liquidLgt.tokenToEthSwapInput(sell_amount, 1, DEADLINE, {'from': accounts[5]}) 56 | 57 | assert liquidLgt.balanceOf(accounts[5]) == account5_supply - sell_amount 58 | assert tx.return_value == int(eth_bought) 59 | assert initial_balance + eth_bought == accounts[5].balance() 60 | assert initial_reserve - eth_bought == liquidLgt.balance() 61 | 62 | 63 | @given(sell_amount=st_sell_amount_token) 64 | @settings(max_examples=5) 65 | def test_sell_lgt_input_to(liquidLgt, accounts, sell_amount): 66 | initial_balance = accounts[3].balance() 67 | eth_bought = token_to_eth_output(sell_amount) 68 | eth_bought_solidity = liquidLgt.getTokenToEthInputPrice(sell_amount) 69 | tx = liquidLgt.tokenToEthTransferInput(sell_amount, 1, DEADLINE, accounts[3], {'from': accounts[5]}) 70 | assert tx.return_value == eth_bought 71 | assert tx.return_value == eth_bought_solidity 72 | assert liquidLgt.balanceOf(accounts[5]) == account5_supply - sell_amount 73 | assert tx.return_value == accounts[3].balance() - initial_balance 74 | 75 | 76 | def test_input_deadline_reverts(liquidLgt, accounts): 77 | with brownie.reverts("dev: deadline passed"): 78 | liquidLgt.tokenToEthSwapInput(1, 1, 1, {'from': accounts[5]}) 79 | 80 | 81 | def test_input_exceed_reverts(liquidLgt, accounts): 82 | with brownie.reverts("LGT: amount exceeds balance"): 83 | liquidLgt.tokenToEthSwapInput(1, 1, DEADLINE, {'from': accounts[6]}) 84 | 85 | 86 | def test_input_tokens_sold_reverts(liquidLgt, accounts): 87 | with brownie.reverts("dev: must sell one or more tokens"): 88 | liquidLgt.tokenToEthSwapInput(0, 1, DEADLINE, {'from': accounts[5]}) 89 | 90 | 91 | def test_input_no_min_eth_reverts(liquidLgt, accounts): 92 | with brownie.reverts("dev: minEth not set"): 93 | liquidLgt.tokenToEthSwapInput(1, 0, DEADLINE, {'from': accounts[5]}) 94 | 95 | 96 | def test_input_minethlow_reverts(liquidLgt, accounts): 97 | with brownie.reverts("dev: tokens not worth enough"): 98 | liquidLgt.tokenToEthSwapInput(1, "100 ether", DEADLINE, {'from': accounts[5]}) 99 | 100 | 101 | def test_input_to_lgt_reverts(liquidLgt, accounts): 102 | with brownie.reverts("dev: can't send to liquid token contract"): 103 | liquidLgt.tokenToEthTransferInput(1, 1, DEADLINE, liquidLgt.address, {'from': accounts[5]}) 104 | 105 | 106 | def test_input_to_zero_reverts(liquidLgt, accounts): 107 | with brownie.reverts("dev: can't send to zero address"): 108 | liquidLgt.tokenToEthTransferInput(1, 1, DEADLINE, ZERO_ADDRESS, {'from': accounts[5]}) 109 | 110 | 111 | @given(buy_amount=st_buy_amount_eth) 112 | @settings(max_examples=20) 113 | def test_sell_lgt_output(liquidLgt, accounts, buy_amount): 114 | initial_balance = accounts[5].balance() 115 | initial_reserve = liquidLgt.balance() 116 | 117 | tokens_sold = token_to_eth_input(buy_amount) 118 | assert tokens_sold == liquidLgt.getTokenToEthOutputPrice(buy_amount) 119 | tx = liquidLgt.tokenToEthSwapOutput(buy_amount, 999, DEADLINE, {'from': accounts[5]}) 120 | 121 | assert liquidLgt.balanceOf(accounts[5]) == account5_supply - tokens_sold 122 | assert initial_balance + buy_amount == accounts[5].balance() 123 | assert initial_reserve - buy_amount == liquidLgt.balance() 124 | assert tx.return_value == tokens_sold 125 | 126 | 127 | @given(buy_amount=st_buy_amount_eth) 128 | @settings(max_examples=5) 129 | def test_sell_lgt_output_to(liquidLgt, accounts, buy_amount): 130 | initial_balance = accounts[3].balance() 131 | initial_reserve = liquidLgt.balance() 132 | 133 | tokens_sold = token_to_eth_input(buy_amount) 134 | assert tokens_sold == liquidLgt.getTokenToEthOutputPrice(buy_amount) 135 | tx = liquidLgt.tokenToEthTransferOutput(buy_amount, 999, DEADLINE, accounts[3], {'from': accounts[5]}) 136 | 137 | assert liquidLgt.balanceOf(accounts[5]) == account5_supply - tokens_sold 138 | assert initial_balance + buy_amount == accounts[3].balance() 139 | assert initial_reserve - buy_amount == liquidLgt.balance() 140 | assert tx.return_value == tokens_sold 141 | 142 | 143 | def test_output_deadline_reverts(liquidLgt, accounts): 144 | with brownie.reverts("dev: deadline passed"): 145 | liquidLgt.tokenToEthSwapOutput(1, 1, 1, {'from': accounts[5]}) 146 | 147 | 148 | def test_output_exceed_reverts(liquidLgt, accounts): 149 | with brownie.reverts("LGT: amount exceeds balance"): 150 | liquidLgt.tokenToEthSwapOutput(1, 1, DEADLINE, {'from': accounts[6]}) 151 | 152 | 153 | def test_output_eth_bought_reverts(liquidLgt, accounts): 154 | with brownie.reverts("dev: must buy more than 0 eth"): 155 | liquidLgt.tokenToEthSwapOutput(0, 1, DEADLINE, {'from': accounts[5]}) 156 | 157 | 158 | def test_output_max_tokens_reverts(liquidLgt, accounts): 159 | with brownie.reverts("dev: need more tokens to sell"): 160 | liquidLgt.tokenToEthSwapOutput(1, 0, DEADLINE, {'from': accounts[5]}) 161 | 162 | 163 | def test_output_to_lgt_reverts(liquidLgt, accounts): 164 | with brownie.reverts("dev: can't send to liquid token contract"): 165 | liquidLgt.tokenToEthTransferOutput(1, 1, DEADLINE, liquidLgt.address, {'from': accounts[5]}) 166 | 167 | 168 | def test_output_to_zero_reverts(liquidLgt, accounts): 169 | with brownie.reverts("dev: can't send to zero address"): 170 | liquidLgt.tokenToEthTransferOutput(1, 1, DEADLINE, ZERO_ADDRESS, {'from': accounts[5]}) 171 | 172 | 173 | def test_transfer_to_self(liquidLgt, accounts): 174 | initial_balance = accounts[5].balance() 175 | initial_reserve = liquidLgt.balance() 176 | 177 | eth_bought = token_to_eth_output(5) 178 | liquidLgt.transfer(liquidLgt, 5, {'from': accounts[5]}) 179 | 180 | assert liquidLgt.balanceOf(accounts[5]) == account5_supply - 5 181 | assert initial_balance + eth_bought == accounts[5].balance() 182 | assert initial_reserve - eth_bought == liquidLgt.balance() 183 | -------------------------------------------------------------------------------- /tests/unit/liquidity/test_buy_lgt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | import pytest 4 | from brownie import * 5 | from brownie.test import given 6 | from hypothesis import settings, strategies as st 7 | 8 | DEADLINE = 99999999999 9 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 10 | FEE_MODIFIER = 995 11 | ADDITIONAL_TOKENS_MINTED = 60 12 | TOKEN_RESERVE = 80 13 | ETHER_RESERVE = Wei("0.079 ether") 14 | 15 | st_amount_eth_to_sell = st.integers(min_value=int(Wei("1 gwei")), max_value=int(Wei("10 ether"))) 16 | st_amount_token_to_buy = st.integers(min_value=1, max_value=int(TOKEN_RESERVE * 0.9)) 17 | 18 | 19 | def eth_to_token_input(eth_to_sell: int) -> "Wei": 20 | """ Constant Price Model implementation with fee. """ 21 | input_amount_with_fee = eth_to_sell * FEE_MODIFIER 22 | numerator = input_amount_with_fee * TOKEN_RESERVE 23 | denominator = ETHER_RESERVE * 1000 + input_amount_with_fee 24 | return Wei(numerator // denominator) 25 | 26 | 27 | def eth_to_token_output(tokens_to_buy: int) -> "Wei": 28 | """ Constant Price Model implementation with fee. """ 29 | numerator = ETHER_RESERVE * tokens_to_buy * 1000 30 | denominator = (TOKEN_RESERVE - tokens_to_buy) * FEE_MODIFIER 31 | return Wei(numerator // denominator + 1) 32 | 33 | 34 | @pytest.fixture(scope="module") 35 | def liquid_lgt(lgt, accounts): 36 | lgt.mint(ADDITIONAL_TOKENS_MINTED, {'from': accounts[0]}) 37 | lgt.addLiquidity( 38 | ETHER_RESERVE - "0.001 ether", 39 | TOKEN_RESERVE - 1, 40 | DEADLINE, 41 | {'from': accounts[0], 'value': ETHER_RESERVE - "0.001 ether"} 42 | ) 43 | yield lgt 44 | 45 | 46 | @given(eth_to_sell=st_amount_eth_to_sell) 47 | @settings(max_examples=50) 48 | def test_swap_input(liquid_lgt, accounts, eth_to_sell): 49 | initial_balance = accounts[2].balance() 50 | initial_reserve = liquid_lgt.balance() 51 | correct_tokens_received = eth_to_token_input(eth_to_sell) 52 | lgt_tokens_received = liquid_lgt.getEthToTokenInputPrice(eth_to_sell) 53 | assert correct_tokens_received == lgt_tokens_received 54 | if correct_tokens_received >= 1: 55 | tx = liquid_lgt.ethToTokenSwapInput(1, DEADLINE, {'from': accounts[2], 'value': eth_to_sell}) 56 | assert liquid_lgt.balanceOf(accounts[2]) == int(correct_tokens_received) 57 | assert tx.return_value == int(correct_tokens_received) 58 | assert initial_balance - eth_to_sell == accounts[2].balance() 59 | assert initial_reserve + eth_to_sell == liquid_lgt.balance() 60 | else: 61 | with brownie.reverts("dev: not enough eth to buy tokens"): 62 | liquid_lgt.ethToTokenSwapInput(1, DEADLINE, {'from': accounts[2], 'value': eth_to_sell}) 63 | 64 | 65 | @given(eth_to_sell=st_amount_eth_to_sell) 66 | @settings(max_examples=5) 67 | def test_transfer_input(liquid_lgt, accounts, eth_to_sell): 68 | correct_tokens_received = eth_to_token_input(eth_to_sell) 69 | lgt_tokens_received = liquid_lgt.getEthToTokenInputPrice(eth_to_sell) 70 | assert correct_tokens_received == lgt_tokens_received 71 | if correct_tokens_received >= 1: 72 | tx = liquid_lgt.ethToTokenTransferInput(1, DEADLINE, accounts[3], {'from': accounts[2], 'value': eth_to_sell}) 73 | assert tx.return_value == correct_tokens_received 74 | assert liquid_lgt.balanceOf(accounts[3]) == correct_tokens_received 75 | else: 76 | with brownie.reverts("dev: not enough eth to buy tokens"): 77 | liquid_lgt.ethToTokenTransferInput(1, DEADLINE, accounts[3], {'from': accounts[2], 'value': eth_to_sell}) 78 | 79 | 80 | def test_input_deadline_reverts(liquid_lgt, accounts): 81 | with brownie.reverts("dev: deadline passed"): 82 | liquid_lgt.ethToTokenTransferInput(1, 1, accounts[3], {'from': accounts[2], 'value': "0.2 ether"}) 83 | 84 | 85 | def test_input_no_eth_reverts(liquid_lgt, accounts): 86 | with brownie.reverts("dev: no eth to sell"): 87 | liquid_lgt.ethToTokenTransferInput(1, DEADLINE, accounts[3], {'from': accounts[2], 'value': "0 ether"}) 88 | 89 | 90 | def test_input_no_min_tokens_reverts(liquid_lgt, accounts): 91 | with brownie.reverts("dev: must buy one or more tokens"): 92 | liquid_lgt.ethToTokenTransferInput(0, DEADLINE, accounts[3], {'from': accounts[2], 'value': "0.2 ether"}) 93 | 94 | 95 | def test_input_not_enough_eth_reverts(liquid_lgt, accounts): 96 | with brownie.reverts("dev: not enough eth to buy tokens"): 97 | liquid_lgt.ethToTokenTransferInput(25, DEADLINE, accounts[3], {'from': accounts[2], 'value': "0.005 ether"}) 98 | 99 | 100 | def test_input_to_lgt_eth_reverts(liquid_lgt, accounts): 101 | with brownie.reverts("dev: can't send to liquid token contract"): 102 | liquid_lgt.ethToTokenTransferInput(25, DEADLINE, liquid_lgt, {'from': accounts[2], 'value': "0.2 ether"}) 103 | 104 | 105 | def test_input_to_zero_eth_reverts(liquid_lgt, accounts): 106 | with brownie.reverts("dev: can't send to zero address"): 107 | liquid_lgt.ethToTokenTransferInput(25, DEADLINE, ZERO_ADDRESS, {'from': accounts[2], 'value': "0.2 ether"}) 108 | 109 | 110 | @given(tokens_to_buy=st_amount_token_to_buy) 111 | @settings(max_examples=10) 112 | def test_swap_output(liquid_lgt, accounts, tokens_to_buy): 113 | initial_balance = accounts[2].balance() 114 | correct_eth_paid = eth_to_token_output(tokens_to_buy) 115 | lgt_eth_paid = liquid_lgt.getEthToTokenOutputPrice(tokens_to_buy) 116 | assert correct_eth_paid == lgt_eth_paid 117 | tx = liquid_lgt.ethToTokenSwapOutput(tokens_to_buy, DEADLINE, {'from': accounts[2], 'value': "50 ether"}) 118 | assert liquid_lgt.balanceOf(accounts[2]) == tokens_to_buy 119 | assert tx.return_value == lgt_eth_paid 120 | assert initial_balance - tx.return_value == accounts[2].balance() 121 | 122 | 123 | def test_swap_output_exact(liquid_lgt, accounts): 124 | correct_eth_paid = eth_to_token_output(4) 125 | expected_eth_paid = liquid_lgt.getEthToTokenOutputPrice(4) 126 | assert correct_eth_paid == expected_eth_paid 127 | tx = liquid_lgt.ethToTokenSwapOutput(4, DEADLINE, {'from': accounts[2], 'value': expected_eth_paid}) 128 | assert liquid_lgt.balanceOf(accounts[2]) == 4 129 | assert tx.return_value == expected_eth_paid 130 | 131 | 132 | @given(tokens_to_buy=st_amount_token_to_buy) 133 | @settings(max_examples=5) 134 | def test_transfer_output(liquid_lgt, accounts, tokens_to_buy): 135 | initial_balance = accounts[2].balance() 136 | correct_eth_paid = eth_to_token_output(tokens_to_buy) 137 | lgt_eth_paid = liquid_lgt.getEthToTokenOutputPrice(tokens_to_buy) 138 | assert correct_eth_paid == lgt_eth_paid 139 | tx = liquid_lgt.ethToTokenTransferOutput(tokens_to_buy, DEADLINE, accounts[3], {'from': accounts[2], 'value': "50 ether"}) 140 | assert liquid_lgt.balanceOf(accounts[3]) == tokens_to_buy 141 | assert tx.return_value == correct_eth_paid 142 | assert initial_balance - tx.return_value == accounts[2].balance() 143 | 144 | 145 | def test_output_deadline_reverts(liquid_lgt, accounts): 146 | with brownie.reverts("dev: deadline passed"): 147 | liquid_lgt.ethToTokenTransferOutput(1, 1, accounts[3], {'from': accounts[2], 'value': "0.2 ether"}) 148 | 149 | 150 | def test_output_no_max_eth_reverts(liquid_lgt, accounts): 151 | with brownie.reverts("dev: maxEth must greater than 0"): 152 | liquid_lgt.ethToTokenTransferOutput(1, DEADLINE, accounts[3], {'from': accounts[2], 'value': "0 ether"}) 153 | 154 | 155 | def test_output_no_min_tokens_reverts(liquid_lgt, accounts): 156 | with brownie.reverts("dev: must buy one or more tokens"): 157 | liquid_lgt.ethToTokenTransferOutput(0, DEADLINE, accounts[3], {'from': accounts[2], 'value': "0.2 ether"}) 158 | 159 | 160 | def test_output_not_enough_eth_reverts(liquid_lgt, accounts): 161 | with brownie.reverts("LGT: not enough ETH"): 162 | liquid_lgt.ethToTokenTransferOutput(25, DEADLINE, accounts[3], {'from': accounts[2], 'value': "0.01 ether"}) 163 | 164 | 165 | def test_output_to_lgt_reverts(liquid_lgt, accounts): 166 | with brownie.reverts("dev: can't send to liquid token contract"): 167 | liquid_lgt.ethToTokenTransferOutput(1, 1, liquid_lgt, {'from': accounts[2], 'value': "0.2 ether"}) 168 | 169 | 170 | def test_output_to_zero_reverts(liquid_lgt, accounts): 171 | with brownie.reverts("dev: can't send to zero address"): 172 | liquid_lgt.ethToTokenTransferOutput(1, 1, ZERO_ADDRESS, {'from': accounts[2], 'value': "0.2 ether"}) 173 | 174 | 175 | def test_fallback(liquid_lgt, accounts): 176 | """ Just sending ether to the contract will send tokens back like buying them. """ 177 | initial_tokens = liquid_lgt.balanceOf(accounts[1]) 178 | expected_tokens = liquid_lgt.getEthToTokenInputPrice("0.005 ether") 179 | accounts[1].transfer(liquid_lgt, "0.005 ether") 180 | assert initial_tokens + expected_tokens == liquid_lgt.balanceOf(accounts[1]) 181 | -------------------------------------------------------------------------------- /contracts/ERC20PointerSupply.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.9; 3 | 4 | import "OpenZeppelin/openzeppelin-contracts@3.0.1/contracts/math/SafeMath.sol"; 5 | import "OpenZeppelin/openzeppelin-contracts@3.0.1/contracts/token/ERC20/IERC20.sol"; 6 | 7 | 8 | /// @title ERC20-Token where total supply is calculated from minted and burned tokens 9 | /// @author Matthias Nadler 10 | contract ERC20PointerSupply is IERC20 { 11 | using SafeMath for uint256; 12 | 13 | // ****** ERC20 Pointer Supply Token 14 | // -------------------------- 15 | // totalSupply is stored in two variables: 16 | // The number of tokens minted and burned, where minted - burned = totalSupply. 17 | // Additionally, the supply is split into: 18 | // - ownedSupply: Number of tokens owned by accounts. 19 | // - tokenReserves: Implicitly defined as totalSupply - ownedSupply, this is the number 20 | // of tokens "owned" by this contract. 21 | // To keep the contract more gas efficient, no Transfer events are emitted when 22 | // minting or burning tokens. 23 | 24 | mapping (address => uint256) internal _balances; 25 | mapping (address => mapping (address => uint256)) internal _allowances; 26 | 27 | uint256 internal _ownedSupply; 28 | uint256 internal _totalBurned; 29 | uint256 internal _totalMinted; 30 | 31 | string constant public name = "Liquid Gas Token"; 32 | string constant public symbol = "LGT"; 33 | uint8 constant public decimals = 0; 34 | 35 | /// @notice Return the total supply of tokens. 36 | /// @dev This is different from a classic ERC20 implementation as the supply is calculated 37 | /// from the burned and minted tokens instead of stored in its own variable. 38 | /// @return Total number of tokens in circulation. 39 | function totalSupply() public view override returns (uint256) { 40 | return _totalMinted.sub(_totalBurned); 41 | } 42 | 43 | /// @notice Return the number of tokens owned by accounts. 44 | /// @dev Unowned tokens belong to this contract and their supply can be 45 | /// calculated implicitly. This means we need to manually track owned tokens, 46 | /// but it makes operations on unowned tokens much more efficient. 47 | /// @return Total number of tokens owned by specific addresses. 48 | function ownedSupply() external view returns (uint256) { 49 | return _ownedSupply; 50 | } 51 | 52 | /// @notice Returns the amount of tokens owned by `account`. 53 | /// @param account The account to query for the balance. 54 | /// @return The amount of tokens owned by `account`. 55 | function balanceOf(address account) public view override returns (uint256) { 56 | return _balances[account]; 57 | } 58 | 59 | /// @notice Moves `amount` tokens from the caller's account to `recipient`. 60 | /// Emits a {Transfer} event. 61 | /// @dev Requirements: 62 | // - `recipient` cannot be the zero address. 63 | // - the caller must have a balance of at least `amount`. 64 | /// @param recipient The tokens are transferred to this address. 65 | /// @param amount The amount of tokens to be transferred. 66 | /// @return True if the transfer succeeded, False otherwise. 67 | function transfer(address recipient, uint256 amount) public override returns (bool) { 68 | _transfer(msg.sender, recipient, amount); 69 | return true; 70 | } 71 | 72 | /// @notice Returns the remaining number of tokens that `spender` will be 73 | /// allowed to spend on behalf of `owner` through {transferFrom}. 74 | /// This is zero by default. 75 | /// @param owner The address that holds the tokens that can be spent by `spender`. 76 | /// @param spender The address that is allowed to spend the tokens held by `owner`. 77 | /// @return Remaining number of tokens that `spender` will be 78 | /// allowed to spend on behalf of `owner` 79 | function allowance(address owner, address spender) public view override returns (uint256) { 80 | return _allowances[owner][spender]; 81 | } 82 | 83 | /// @notice Sets `amount` as the allowance of `spender` over the caller's tokens. 84 | /// Emits an {Approval} event. 85 | /// @dev IMPORTANT: Beware that changing an allowance with this method brings the risk 86 | /// that someone may use both the old and the new allowance by unfortunate 87 | /// transaction ordering. This contracts provides {increaseAllowance} and 88 | /// {decreaseAllowance} to mitigate this problem. See: 89 | /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 90 | /// Requirements: 91 | /// - `spender` cannot be the zero address. 92 | /// @param spender The address that is allowed to spend the tokens held by the caller. 93 | /// @param amount The amount of tokens the `spender` can spend from the caller's supply. 94 | /// @return True if the approval succeeded, False otherwise. 95 | function approve(address spender, uint256 amount) public override returns (bool) { 96 | _approve(msg.sender, spender, amount); 97 | return true; 98 | } 99 | 100 | 101 | /// @notice Moves `amount` tokens from `sender` to `recipient` using the allowance 102 | /// mechanism. `amount` is then deducted from the caller's allowance. 103 | /// Emits a {Transfer} and an {Approval} event. 104 | /// @dev Requirements: 105 | /// - `sender` and `recipient` cannot be the zero address. 106 | /// - `sender` must have a balance of at least `amount`. 107 | /// - the caller must have allowance for `sender`'s tokens of at least `amount`. 108 | /// @param sender The tokens are transferred from this address. 109 | /// @param recipient The tokens are transferred to this address. 110 | /// @param amount The amount of tokens to be transferred. 111 | /// @return True if the transfer succeeded, False otherwise. 112 | function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { 113 | _transfer(sender, recipient, amount); 114 | _approve( 115 | sender, 116 | msg.sender, 117 | _allowances[sender][msg.sender].sub(amount, "ERC20: exceeds allowance") 118 | ); 119 | return true; 120 | } 121 | 122 | /// @notice Atomically increases the allowance granted to `spender` by the caller. 123 | /// This is an alternative to {approve} that can be used as a mitigation for 124 | /// problems described in {approve}. 125 | /// Emits an {Approval} event. 126 | /// @dev Requirements: 127 | /// - `spender` cannot be the zero address. 128 | /// @param spender The address that is allowed to spend the tokens held by the caller. 129 | /// @param addedValue The amount of tokens to add to the current `allowance`. 130 | /// @return True if the approval succeeded, False otherwise. 131 | function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { 132 | _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue)); 133 | return true; 134 | } 135 | 136 | /// @notice Atomically decreases the allowance granted to `spender` by the caller. 137 | /// This is an alternative to {approve} that can be used as a mitigation for 138 | /// problems described in {approve}. 139 | /// Emits an {Approval} event. 140 | /// @dev Requirements: 141 | /// - `spender` cannot be the zero address. 142 | /// - `spender` must have allowance for the caller of at least `subtractedValue`. 143 | /// @param spender The address that is allowed to spend the tokens held by the caller. 144 | /// @param subtractedValue The amount of tokens to subtract from the current `allowance`. 145 | /// @return True if the approval succeeded, False otherwise. 146 | function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { 147 | _approve( 148 | msg.sender, 149 | spender, 150 | _allowances[msg.sender][spender].sub(subtractedValue, "ERC20: allowance below zero") 151 | ); 152 | return true; 153 | } 154 | 155 | 156 | // ****** Internal ERC20 Functions 157 | // ------------------------ 158 | 159 | /// @dev Triggered when tokens are transferred to this contract. 160 | /// Can be overridden by an implementation to allow and handle this behaviour. 161 | /// This should emit a {Transfer} event if an ownership change is made. 162 | function _transferToSelf(address sender, uint256 amount) internal virtual { 163 | revert("ERC20: transfer to contract"); 164 | } 165 | 166 | function _transfer(address sender, address recipient, uint256 amount) internal virtual { 167 | require(recipient != address(0), "ERC20: transfer to zero address"); 168 | if (recipient == address(this)) { 169 | _transferToSelf(sender, amount); 170 | } else { 171 | _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer exceeds balance"); 172 | _balances[recipient] += amount; 173 | emit Transfer(sender, recipient, amount); 174 | } 175 | } 176 | 177 | function _approve(address owner, address spender, uint256 amount) internal virtual { 178 | _allowances[owner][spender] = amount; 179 | emit Approval(owner, spender, amount); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /contracts/LiquidGasToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.9; 3 | 4 | import "./LiquidERC20.sol"; 5 | 6 | /// @title The Liquid Gas Token. An ERC20 Gas Token with integrated liquidity pool. 7 | /// Allows for efficient ownership transfers and lower cost when buying or selling. 8 | /// @author Matthias Nadler 9 | contract LiquidGasToken is LiquidERC20 { 10 | 11 | // ***** Gas Token Core 12 | // -------------- 13 | // Create and destroy contracts 14 | 15 | /// @dev Create `amount` contracts that can be destroyed by this contract. 16 | /// Pass _totalMinted as `i` 17 | function _createContracts(uint256 amount, uint256 i) internal { 18 | assembly { 19 | let end := add(i, amount) 20 | mstore(0, 21 | add( 22 | add( 23 | 0x746d000000000000000000000000000000000000000000000000000000000000, 24 | shl(0x80, address()) 25 | ), 26 | 0x3318585733ff6000526015600bf30000 27 | ) 28 | ) 29 | for {let j := div(amount, 32)} j {j := sub(j, 1)} { 30 | pop(create2(0, 0, 30, add(i, 0))) pop(create2(0, 0, 30, add(i, 1))) 31 | pop(create2(0, 0, 30, add(i, 2))) pop(create2(0, 0, 30, add(i, 3))) 32 | pop(create2(0, 0, 30, add(i, 4))) pop(create2(0, 0, 30, add(i, 5))) 33 | pop(create2(0, 0, 30, add(i, 6))) pop(create2(0, 0, 30, add(i, 7))) 34 | pop(create2(0, 0, 30, add(i, 8))) pop(create2(0, 0, 30, add(i, 9))) 35 | pop(create2(0, 0, 30, add(i, 10))) pop(create2(0, 0, 30, add(i, 11))) 36 | pop(create2(0, 0, 30, add(i, 12))) pop(create2(0, 0, 30, add(i, 13))) 37 | pop(create2(0, 0, 30, add(i, 14))) pop(create2(0, 0, 30, add(i, 15))) 38 | pop(create2(0, 0, 30, add(i, 16))) pop(create2(0, 0, 30, add(i, 17))) 39 | pop(create2(0, 0, 30, add(i, 18))) pop(create2(0, 0, 30, add(i, 19))) 40 | pop(create2(0, 0, 30, add(i, 20))) pop(create2(0, 0, 30, add(i, 21))) 41 | pop(create2(0, 0, 30, add(i, 22))) pop(create2(0, 0, 30, add(i, 23))) 42 | pop(create2(0, 0, 30, add(i, 24))) pop(create2(0, 0, 30, add(i, 25))) 43 | pop(create2(0, 0, 30, add(i, 26))) pop(create2(0, 0, 30, add(i, 27))) 44 | pop(create2(0, 0, 30, add(i, 28))) pop(create2(0, 0, 30, add(i, 29))) 45 | pop(create2(0, 0, 30, add(i, 30))) pop(create2(0, 0, 30, add(i, 31))) 46 | i := add(i, 32) 47 | } 48 | 49 | for { } lt(i, end) { i := add(i, 1) } { 50 | pop(create2(0, 0, 30, i)) 51 | } 52 | sstore(_totalMinted_slot, end) 53 | } 54 | } 55 | 56 | /// @dev calculate the address of a child contract given its salt 57 | function computeAddress2(uint256 salt) external view returns (address child) { 58 | assembly { 59 | let data := mload(0x40) 60 | mstore(data, 61 | add( 62 | 0xff00000000000000000000000000000000000000000000000000000000000000, 63 | shl(0x58, address()) 64 | ) 65 | ) 66 | mstore(add(data, 21), salt) 67 | mstore(add(data, 53), 68 | add( 69 | add( 70 | 0x746d000000000000000000000000000000000000000000000000000000000000, 71 | shl(0x80, address()) 72 | ), 73 | 0x3318585733ff6000526015600bf30000 74 | ) 75 | ) 76 | mstore(add(data, 53), keccak256(add(data, 53), 30)) 77 | child := and(keccak256(data, 85), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) 78 | } 79 | } 80 | 81 | /// @dev Destroy `amount` contracts and free the gas. 82 | /// Pass _totalBurned as `i` 83 | function _destroyContracts(uint256 amount, uint256 i) internal { 84 | assembly { 85 | let end := add(i, amount) 86 | 87 | let data := mload(0x40) 88 | mstore(data, 89 | add( 90 | 0xff00000000000000000000000000000000000000000000000000000000000000, 91 | shl(0x58, address()) 92 | ) 93 | ) 94 | mstore(add(data, 53), 95 | add( 96 | add( 97 | 0x746d000000000000000000000000000000000000000000000000000000000000, 98 | shl(0x80, address()) 99 | ), 100 | 0x3318585733ff6000526015600bf30000 101 | ) 102 | ) 103 | mstore(add(data, 53), keccak256(add(data, 53), 30)) 104 | let ptr := add(data, 21) 105 | for { } lt(i, end) { i := add(i, 1) } { 106 | mstore(ptr, i) 107 | pop(call(gas(), keccak256(data, 85), 0, 0, 0, 0, 0)) 108 | } 109 | 110 | sstore(_totalBurned_slot, end) 111 | } 112 | } 113 | 114 | // *** Constructor 115 | 116 | // @dev: Set initial liquidity. Must mint at least 1 token to the pool. 117 | constructor() public { 118 | _createContracts(1, 0); 119 | } 120 | 121 | // ***** Gas Token Minting 122 | // ----------------- 123 | // Different ways to mint Gas Tokens 124 | 125 | 126 | // *** Mint to owner 127 | 128 | /// @notice Mint personally owned Liquid Gas Tokens. 129 | /// @param amount The amount of tokens to mint. 130 | function mint(uint256 amount) external { 131 | _createContracts(amount, _totalMinted); 132 | _balances[msg.sender] += amount; 133 | _ownedSupply += amount; 134 | } 135 | 136 | /// @notice Mint Liquid Gas Tokens for `recipient`. 137 | /// @param amount The amount of tokens to mint. 138 | /// @param recipient The owner of the minted Liquid Gas Tokens. 139 | function mintFor(uint256 amount, address recipient) external { 140 | _createContracts(amount, _totalMinted); 141 | _balances[recipient] += amount; 142 | _ownedSupply += amount; 143 | } 144 | 145 | // *** Mint to liquidity pool 146 | 147 | /// @notice Mint Liquid Gas Tokens and add them to the Liquidity Pool. 148 | /// The amount of tokens minted and added to the pool is calculated 149 | /// from the amount of ether sent and `maxTokens`. 150 | /// The liquidity shares are created for the `recipient`. 151 | /// Emits an {AddLiquidity} event. 152 | /// @dev This is much more efficient than minting tokens and adding them 153 | /// to the liquidity pool in two separate steps. 154 | /// Excess ether that is not added to the pool will be refunded. 155 | /// Requirements: 156 | /// - `recipient` can't be this contract or the zero address 157 | /// @param maxTokens The maximum amount of tokens that will be minted. 158 | /// Set this to cap the gas the transaction will use. 159 | /// If more than maxTokens could be created, the remaining ether is refunded. 160 | /// @param minLiquidity The minimum amount of liquidity shares to create, 161 | /// will revert if not enough liquidity can be created. 162 | /// @param deadline The time after which the transaction can no longer be executed. 163 | /// Will revert if the current timestamp is after the deadline. 164 | /// @param recipient Liquidity shares are created for this address. 165 | /// @return tokenAmount Amount of tokens minted and invested. 166 | /// @return ethAmount Amount of ether invested. 167 | /// @return liquidityCreated Number of liquidity shares created. 168 | function mintToLiquidity( 169 | uint256 maxTokens, 170 | uint256 minLiquidity, 171 | uint256 deadline, 172 | address recipient 173 | ) 174 | external 175 | payable 176 | returns (uint256 tokenAmount, uint256 ethAmount, uint256 liquidityCreated) 177 | { 178 | require(deadline >= now); // dev: deadline passed 179 | require(maxTokens != 0); // dev: can't mint less than 1 token 180 | require(msg.value != 0); // dev: must provide ether to add liquidity 181 | 182 | // calculate optimum values for tokens and ether to add 183 | uint256 totalMinted = _totalMinted; 184 | tokenAmount = maxTokens; 185 | uint256 tokenReserve = totalMinted.sub(_totalBurned + _ownedSupply); 186 | uint ethReserve = address(this).balance - msg.value; 187 | ethAmount = (tokenAmount.mul(ethReserve) / tokenReserve).sub(1); 188 | if (ethAmount > msg.value) { 189 | // reduce amount of tokens minted to provide maximum possible liquidity 190 | tokenAmount = (msg.value + 1).mul(tokenReserve) / ethReserve; 191 | ethAmount = (tokenAmount.mul(ethReserve) / tokenReserve).sub(1); 192 | } 193 | uint256 totalLiquidity = _poolTotalSupply; 194 | liquidityCreated = ethAmount.mul(totalLiquidity) / ethReserve; 195 | require(liquidityCreated >= minLiquidity); // dev: not enough liquidity can be created 196 | 197 | // Mint tokens directly to the liquidity pool 198 | _createContracts(tokenAmount, totalMinted); 199 | 200 | // Create liquidity shares for recipient 201 | _poolTotalSupply = totalLiquidity + liquidityCreated; 202 | _poolBalances[recipient] += liquidityCreated; 203 | 204 | emit AddLiquidity(recipient, ethAmount, tokenAmount); 205 | 206 | // refund excess ether 207 | if (msg.value > ethAmount) { 208 | msg.sender.call{value: msg.value - ethAmount}(""); 209 | } 210 | return (tokenAmount, ethAmount, liquidityCreated); 211 | } 212 | 213 | // *** Mint to sell 214 | 215 | /// @notice Mint Liquid Gas Tokens, immediately sell them for ether and 216 | /// transfer the ether to the `recipient`. 217 | /// @dev This is much more efficient than minting tokens and then selling them 218 | /// in two separate steps. 219 | /// @param amount The amount of tokens to mint and sell. 220 | /// @param minEth The minimum amount of ether to receive for the transaction. 221 | /// Will revert if the tokens don't sell for enough ether; 222 | /// The gas for minting is not used. 223 | /// @param deadline The time after which the transaction can no longer be executed. 224 | /// Will revert if the current timestamp is after the deadline. 225 | /// @return The amount of ether received from the sale. 226 | function mintToSellTo( 227 | uint256 amount, 228 | uint256 minEth, 229 | uint256 deadline, 230 | address payable recipient 231 | ) 232 | public 233 | returns (uint256) 234 | { 235 | require(deadline >= now); // dev: deadline passed 236 | require(amount != 0); // dev: must sell one or more tokens 237 | uint256 totalMinted = _totalMinted; 238 | uint256 tokenReserve = totalMinted.sub(_totalBurned + _ownedSupply); 239 | uint256 ethBought = getInputPrice(amount, tokenReserve, address(this).balance); 240 | require(ethBought >= minEth); // dev: tokens not worth enough 241 | _createContracts(amount, totalMinted); 242 | recipient.call{value: ethBought}(""); 243 | return ethBought; 244 | } 245 | 246 | /// @notice Mint Liquid Gas Tokens and immediately sell them for ether. 247 | /// @dev This is much more efficient than minting tokens and then selling them 248 | /// in two separate steps. 249 | /// @param amount The amount of tokens to mint and sell. 250 | /// @param minEth The minimum amount of ether to receive for the transaction. 251 | /// Will revert if the tokens don't sell for enough ether; 252 | /// The gas for minting is not used. 253 | /// @param deadline The time after which the transaction can no longer be executed. 254 | /// Will revert if the current timestamp is after the deadline. 255 | /// @return The amount of ether received from the sale. 256 | function mintToSell( 257 | uint256 amount, 258 | uint256 minEth, 259 | uint256 deadline 260 | ) 261 | external 262 | returns (uint256) 263 | { 264 | return mintToSellTo(amount, minEth, deadline, msg.sender); 265 | } 266 | 267 | // ***** Gas Token Freeing 268 | // ----------------- 269 | // Different ways to free Gas Tokens 270 | 271 | 272 | // *** Free owned tokens 273 | 274 | /// @notice Free `amount` of Liquid Gas Tokens from the `sender`'s balance. 275 | /// @param amount The amount of tokens to free 276 | /// @return True if `tokenAmount` tokens could be freed, False otherwise. 277 | function free(uint256 amount) external returns (bool) { 278 | uint256 balance = _balances[msg.sender]; 279 | if (balance < amount) { 280 | return false; 281 | } 282 | _balances[msg.sender] = balance - amount; 283 | _ownedSupply = _ownedSupply.sub(amount); 284 | _destroyContracts(amount, _totalBurned); 285 | return true; 286 | } 287 | 288 | /// @notice Free `amount` of Liquid Gas Tokens from the `owners`'s balance. 289 | /// @param amount The amount of tokens to free 290 | /// @param owner The `owner` of the tokens. The `sender` must have an allowance. 291 | /// @return True if `tokenAmount` tokens could be freed, False otherwise. 292 | function freeFrom(uint256 amount, address owner) external returns (bool) { 293 | uint256 balance = _balances[owner]; 294 | if (balance < amount) { 295 | return false; 296 | } 297 | uint256 currentAllowance = _allowances[owner][msg.sender]; 298 | if (currentAllowance < amount) { 299 | return false; 300 | } 301 | _balances[owner] = balance - amount; 302 | _ownedSupply = _ownedSupply.sub(amount); 303 | _approve(owner, msg.sender, currentAllowance - amount); 304 | _destroyContracts(amount, _totalBurned); 305 | return true; 306 | } 307 | 308 | // *** Free from liquidity pool 309 | 310 | /// @notice Buy `amount` tokens from the liquidity pool and immediately free them. 311 | /// @param amount The amount of tokens to buy and free. 312 | /// @param deadline The time after which the transaction can no longer be executed. 313 | /// Will revert if the current timestamp is after the deadline. 314 | /// @param refundTo Any excess ether will be refunded to this address. 315 | /// @dev This will not revert unless an unexpected error occurs. Instead it will return 0. 316 | /// @return The amount of ether spent to buy `amount` tokens. 317 | function buyAndFree( 318 | uint256 amount, 319 | uint256 deadline, 320 | address payable refundTo 321 | ) 322 | external 323 | payable 324 | returns (uint256) 325 | { 326 | if (deadline < now) { 327 | refundTo.call{value: msg.value}(""); 328 | return 0; 329 | } 330 | uint256 totalBurned = _totalBurned; 331 | uint256 tokenReserve = _totalMinted.sub(totalBurned + _ownedSupply); 332 | if (tokenReserve < amount) { 333 | refundTo.call{value: msg.value}(""); 334 | return 0; 335 | } 336 | uint256 ethReserve = address(this).balance - msg.value; 337 | uint256 ethSold = getOutputPrice(amount, ethReserve, tokenReserve); 338 | if (msg.value < ethSold) { 339 | refundTo.call{value: msg.value}(""); 340 | return 0; 341 | } 342 | uint256 ethRefund = msg.value - ethSold; 343 | _destroyContracts(amount, totalBurned); 344 | if (ethRefund != 0) { 345 | refundTo.call{value: ethRefund}(""); 346 | } 347 | return ethSold; 348 | } 349 | 350 | /// @notice Buy as many tokens as possible from the liquidity pool and immediately free them. 351 | /// Will buy less than `maxTokens` if not enough ether is provided. 352 | /// Excess ether is not refunded! 353 | /// @param deadline The time after which the transaction can no longer be executed. 354 | /// Will revert if the current timestamp is after the deadline. 355 | /// @dev Will revert if deadline passed to refund the ether. 356 | /// @return The amount of tokens bought and freed. 357 | function buyMaxAndFree(uint256 deadline) 358 | external 359 | payable 360 | returns (uint256) 361 | { 362 | require(deadline >= now); // dev: deadline passed 363 | uint256 ethReserve = address(this).balance - msg.value; 364 | uint256 totalBurned = _totalBurned; 365 | uint256 tokenReserve = _totalMinted.sub(totalBurned + _ownedSupply); 366 | uint256 tokensBought = getInputPrice(msg.value, ethReserve, tokenReserve); 367 | _destroyContracts(tokensBought, totalBurned); 368 | return tokensBought; 369 | } 370 | 371 | // ***** Deployment Functions 372 | // ------------------ 373 | // Execute a deployment while buying tokens and freeing them. 374 | 375 | 376 | /// @notice Deploy a contract via create() while buying and freeing `tokenAmount` tokens 377 | /// to reduce the gas cost. You need to provide ether to buy the tokens. 378 | /// Any excess ether is refunded. 379 | /// @param tokenAmount The number of tokens bought and freed. 380 | /// @param deadline The time after which the transaction can no longer be executed. 381 | /// Will revert if the current timestamp is after the deadline. 382 | /// @param bytecode The bytecode of the contract you want to deploy. 383 | /// @dev Will revert if deadline passed or not enough ether is sent. 384 | /// Can't send ether with deployment. Pre-fund the address instead. 385 | /// @return contractAddress The address where the contract was deployed. 386 | 387 | function deploy(uint256 tokenAmount, uint256 deadline, bytes memory bytecode) 388 | external 389 | payable 390 | returns (address contractAddress) 391 | { 392 | require(deadline >= now); // dev: deadline passed 393 | uint256 totalBurned = _totalBurned; 394 | uint256 tokenReserve = _totalMinted.sub(totalBurned + _ownedSupply); 395 | uint256 price = getOutputPrice(tokenAmount, address(this).balance - msg.value, tokenReserve); 396 | uint256 refund = msg.value.sub(price, "LGT: insufficient ether"); 397 | _destroyContracts(tokenAmount, totalBurned); 398 | 399 | if (refund > 0) { 400 | msg.sender.call{value: refund}(""); 401 | } 402 | assembly { 403 | contractAddress := create(0, add(bytecode, 32), mload(bytecode)) 404 | } 405 | return contractAddress; 406 | } 407 | 408 | /// @notice Deploy a contract via create2() while buying and freeing `tokenAmount` tokens 409 | /// to reduce the gas cost. You need to provide ether to buy the tokens. 410 | /// Any excess ether is refunded. 411 | /// @param tokenAmount The number of tokens bought and freed. 412 | /// @param deadline The time after which the transaction can no longer be executed. 413 | /// Will revert if the current timestamp is after the deadline. 414 | /// @param salt The salt is used for create2() to determine the deployment address. 415 | /// @param bytecode The bytecode of the contract you want to deploy. 416 | /// @dev Will revert if deadline passed or not enough ether is sent. 417 | /// Can't send ether with deployment. Pre-fund the address instead. 418 | /// @return contractAddress The address where the contract was deployed. 419 | function create2(uint256 tokenAmount, uint256 deadline, uint256 salt, bytes memory bytecode) 420 | external 421 | payable 422 | returns (address contractAddress) 423 | { 424 | require(deadline >= now); // dev: deadline passed 425 | uint256 totalBurned = _totalBurned; 426 | uint256 tokenReserve = _totalMinted.sub(totalBurned + _ownedSupply); 427 | uint256 price = getOutputPrice(tokenAmount, address(this).balance - msg.value, tokenReserve); 428 | uint256 refund = msg.value.sub(price, "LGT: insufficient ether"); 429 | _destroyContracts(tokenAmount, totalBurned); 430 | 431 | if (refund > 0) { 432 | msg.sender.call{value: refund}(""); 433 | } 434 | assembly { 435 | contractAddress := create2(0, add(bytecode, 32), mload(bytecode), salt) 436 | } 437 | return contractAddress; 438 | } 439 | 440 | // ***** Advanced Functions !!! USE AT YOUR OWN RISK !!! 441 | // ----------------------------------------------- 442 | // These functions are gas optimized and intended for experienced users. 443 | // The function names are constructed to have 3 or 4 leading zero bytes 444 | // in the function selector. 445 | // Additionally, all checks have been omitted and need to be done before 446 | // sending the call if desired. 447 | // There are also no return values to further save gas. 448 | 449 | 450 | /// @notice Mint Liquid Gas Tokens and immediately sell them for ether. 451 | /// @dev 3 zero bytes function selector (0x000000079) and removed all checks. 452 | /// !!! USE AT YOUR OWN RISK !!! 453 | /// @param amount The amount of tokens to mint and sell. 454 | function mintToSell9630191(uint256 amount) external { 455 | uint256 totalMinted = _totalMinted; 456 | uint256 ethBought = getInputPrice( 457 | amount, 458 | totalMinted.sub(_totalBurned + _ownedSupply), 459 | address(this).balance 460 | ); 461 | _createContracts(amount, totalMinted); 462 | msg.sender.call{value: ethBought}(""); 463 | } 464 | 465 | /// @notice Mint Liquid Gas Tokens, immediately sell them for ether and 466 | /// transfer the ether to the `recipient`. 467 | /// @dev 3 zero bytes function selector (0x00000056) and removed all checks. 468 | /// !!! USE AT YOUR OWN RISK !!! 469 | /// @param amount The amount of tokens to mint and sell. 470 | /// @param recipient The address the ether is sent to 471 | function mintToSellTo25630722(uint256 amount, address payable recipient) external { 472 | uint256 totalMinted = _totalMinted; 473 | uint256 ethBought = getInputPrice( 474 | amount, 475 | totalMinted.sub(_totalBurned + _ownedSupply), 476 | address(this).balance 477 | ); 478 | _createContracts(amount, totalMinted); 479 | recipient.call{value: ethBought}(""); 480 | } 481 | 482 | 483 | /// @notice Buy `amount` tokens from the liquidity pool and immediately free them. 484 | /// Make sure to pass the exact amount for tokens and sent ether: 485 | /// - There are no refunds for unspent ether! 486 | /// - Get the exact price by calling getEthToTokenOutputPrice(`amount`) 487 | /// before sending the call in the same transaction. 488 | /// @dev 4 zero bytes function selector (0x00000000) and removed all checks. 489 | /// !!! USE AT YOUR OWN RISK !!! 490 | /// @param amount The amount of tokens to buy and free. 491 | function buyAndFree22457070633(uint256 amount) external payable { 492 | uint256 totalBurned = _totalBurned; 493 | uint256 ethSold = getOutputPrice( 494 | amount, 495 | address(this).balance - msg.value, 496 | _totalMinted.sub(totalBurned + _ownedSupply) 497 | ); 498 | if (msg.value >= ethSold) { 499 | _destroyContracts(amount, totalBurned); 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /contracts/LiquidERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.9; 3 | 4 | import "./ERC20PointerSupply.sol"; 5 | 6 | /// @title ERC20-Token with built in Liquidity Pool 7 | /// @dev The Liquidity Shares do not adhere to ERC20 standards, 8 | /// only the underlying token does. Liquidity can not be traded. 9 | /// @author Matthias Nadler 10 | contract LiquidERC20 is ERC20PointerSupply { 11 | 12 | // ***** Liquidity Pool 13 | // -------------- 14 | // Integrated Liquidity Pool for an ERC20 Pointer Supply Token. 15 | // More efficient due to shortcuts in the ownership transfers. 16 | // Modelled after Uniswap V1 by Hayden Adams: 17 | // https://github.com/Uniswap/uniswap-v1/blob/master/contracts/uniswap_exchange.vy 18 | // Sell and Buy events are not implemented in the interest of gas efficiency. 19 | // Liquidity shares do not adhere to ERC20 specifications. 20 | // However, a subset of ERC20-like functions are implemented. 21 | 22 | uint256 internal _poolTotalSupply; 23 | mapping (address => uint256) internal _poolBalances; 24 | 25 | event AddLiquidity( 26 | address indexed provider, 27 | uint256 indexed eth_amount, 28 | uint256 indexed token_amount 29 | ); 30 | event RemoveLiquidity( 31 | address indexed provider, 32 | uint256 indexed eth_amount, 33 | uint256 indexed token_amount 34 | ); 35 | event TransferLiquidity( 36 | address indexed from, 37 | address indexed to, 38 | uint256 value 39 | ); 40 | 41 | /// @notice Returns the amount of liquidity shares owned by `account`. 42 | /// @param account The account to query for the balance. 43 | /// @return The amount of liquidity shares owned by `account`. 44 | function poolBalanceOf(address account) external view returns (uint256) { 45 | return _poolBalances[account]; 46 | } 47 | 48 | /// @notice Return the total supply of liquidity shares. 49 | /// @return Total number of liquidity shares. 50 | function poolTotalSupply() external view returns (uint256) { 51 | return _poolTotalSupply; 52 | } 53 | 54 | /// @notice The amount of tokens in the liquidity pool. 55 | /// @dev This is defined implicitly as the difference between 56 | /// The total supply and the privately owned supply of the token. 57 | /// @return The amount of tokens in the liquidity pool. 58 | function poolTokenReserves() external view returns (uint256) { 59 | return _totalMinted.sub(_totalBurned + _ownedSupply); 60 | } 61 | 62 | /// @notice Moves `amount` liquidity shares from the caller's account to `recipient`. 63 | /// Emits a {Transfer} event. 64 | /// @dev Requirements: 65 | // - `recipient` cannot be the zero address. 66 | // - the caller must have a balance of at least `amount`. 67 | /// @param recipient The tokens are transferred to this address. 68 | /// @param amount The amount of tokens to be transferred. 69 | /// @return True if the transfer succeeded, False otherwise. 70 | function poolTransfer(address recipient, uint256 amount) external returns (bool) { 71 | require(recipient != address(0)); // dev: can't transfer liquidity to zero address 72 | require(recipient != address(this)); // dev: can't transfer liquidity to token contract 73 | _poolBalances[msg.sender] = _poolBalances[msg.sender].sub(amount, "LGT: transfer exceeds balance"); 74 | _poolBalances[recipient]= _poolBalances[recipient].add(amount); 75 | emit TransferLiquidity(msg.sender, recipient, amount); 76 | return true; 77 | } 78 | 79 | // *** Constructor 80 | /// @dev Start with initial liquidity. Contract must be pre-funded. 81 | /// This initial liquidity must never be removed. 82 | constructor() public { 83 | // Implementation must mint at least 1 token to the pool during deployment. 84 | uint ethReserve = address(this).balance; 85 | require(ethReserve > 1000000000); 86 | _poolTotalSupply += ethReserve; 87 | _poolBalances[msg.sender] += ethReserve; 88 | } 89 | 90 | // ***** Liquidity Pool 91 | // -------------------- 92 | // Add, remove or transfer liquidity shares. 93 | 94 | /// @notice Add liquidity to the pool and receive liquidity shares. Must deposit 95 | /// an equal amount of ether and tokens at the current exchange rate. 96 | /// Emits an {AddLiquidity} event. 97 | /// @param minLiquidity The minimum amount of liquidity shares to create, 98 | /// will revert if not enough liquidity can be created. 99 | /// @param maxTokens The maximum amount of tokens to transfer to match the provided 100 | /// ether liquidity. Will revert if too many tokens are needed. 101 | /// @param deadline The time after which the transaction can no longer be executed. 102 | /// Will revert if the current timestamp is after the deadline. 103 | /// @return The amount of liquidity shares created. 104 | function addLiquidity(uint256 minLiquidity, uint256 maxTokens, uint256 deadline) 105 | external 106 | payable 107 | returns (uint256) 108 | { 109 | require(deadline >= now); // dev: deadline passed 110 | require(maxTokens != 0); // dev: no tokens to add 111 | require(msg.value != 0); // dev: no ether to add 112 | require(minLiquidity != 0); // dev: no min_liquidity specified 113 | 114 | uint256 ethReserve = address(this).balance - msg.value; 115 | uint256 ownedSupply = _ownedSupply; 116 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + ownedSupply); 117 | uint256 tokenAmount = msg.value.mul(tokenReserve) / ethReserve + 1; 118 | uint256 poolTotalSupply = _poolTotalSupply; 119 | uint256 liquidityCreated = msg.value.mul(poolTotalSupply) / ethReserve; 120 | require(maxTokens >= tokenAmount); // dev: need more tokens 121 | require(liquidityCreated >= minLiquidity); // dev: not enough liquidity can be created 122 | 123 | // create liquidity shares 124 | _poolTotalSupply = poolTotalSupply + liquidityCreated; 125 | _poolBalances[msg.sender] += liquidityCreated; 126 | 127 | // remove LGTs from sender 128 | _balances[msg.sender] = _balances[msg.sender].sub( 129 | tokenAmount, "LGT: amount exceeds balance" 130 | ); 131 | _ownedSupply = ownedSupply.sub(tokenAmount); 132 | 133 | emit AddLiquidity(msg.sender, msg.value, tokenAmount); 134 | return liquidityCreated; 135 | } 136 | 137 | 138 | /// @notice Remove liquidity shares and receive an equal amount of tokens and ether 139 | /// at the current exchange rate from the liquidity pool. 140 | /// Emits a {RemoveLiquidity} event. 141 | /// @param amount The amount of liquidity shares to remove from the pool. 142 | /// @param minEth The minimum amount of ether you want to receive in the transaction. 143 | /// Will revert if less than `minEth` ether would be transferred. 144 | /// @param minTokens The minimum amount of tokens you want to receive in the transaction. 145 | /// Will revert if less than `minTokens` tokens would be transferred. 146 | /// @param deadline The time after which the transaction can no longer be executed. 147 | /// Will revert if the current timestamp is after the deadline. 148 | /// @dev Requirements: 149 | /// - `sender` must have a liquidity pool balance of at least `amount`. 150 | /// @return The amount of ether and tokens refunded. 151 | function removeLiquidity(uint256 amount, uint256 minEth, uint256 minTokens, uint256 deadline) 152 | external 153 | returns (uint256, uint256) 154 | { 155 | require(deadline >= now); // dev: deadline passed 156 | require(amount != 0); // dev: amount of liquidity to remove must be positive 157 | require(minEth != 0); // dev: must remove positive eth amount 158 | require(minTokens != 0); // dev: must remove positive token amount 159 | uint256 totalLiquidity = _poolTotalSupply; 160 | uint256 ownedSupply = _ownedSupply; 161 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + ownedSupply); 162 | uint256 ethAmount = amount.mul(address(this).balance) / totalLiquidity; 163 | uint256 tokenAmount = amount.mul(tokenReserve) / totalLiquidity; 164 | require(ethAmount >= minEth); // dev: can't remove enough eth 165 | require(tokenAmount >= minTokens); // dev: can't remove enough tokens 166 | 167 | // Remove liquidity shares 168 | _poolBalances[msg.sender] = _poolBalances[msg.sender].sub(amount); 169 | _poolTotalSupply = totalLiquidity.sub(amount); 170 | 171 | // Transfer tokens 172 | _balances[msg.sender] += tokenAmount; 173 | _ownedSupply = ownedSupply + tokenAmount; 174 | 175 | emit RemoveLiquidity(msg.sender, ethAmount, tokenAmount); 176 | 177 | // Transfer ether 178 | msg.sender.call{value: ethAmount}(""); 179 | 180 | return (ethAmount, tokenAmount); 181 | } 182 | 183 | // ***** Constant Price Model 184 | // -------------------- 185 | // Internal price calculation functions for the constant price model with fees. 186 | 187 | 188 | /// @dev token reserve and pool balance are guaranteed to be non-zero 189 | /// No need to require inputReserve != 0 190 | function getInputPrice(uint256 inputAmount, uint256 inputReserve, uint256 outputReserve) 191 | internal 192 | pure 193 | returns (uint256) 194 | { 195 | uint256 inputAmountWithFee = inputAmount.mul(995); 196 | uint256 numerator = inputAmountWithFee.mul(outputReserve); 197 | uint256 denominator = inputReserve.mul(1000).add(inputAmountWithFee); 198 | return numerator / denominator; 199 | } 200 | 201 | /// @dev Requirements: 202 | /// - `OutputAmount` must be greater than `OutputReserve` 203 | /// Token reserve and pool balance are guaranteed to be non-zero 204 | /// No need to require inputReserve != 0 or outputReserve != 0 205 | function getOutputPrice(uint256 outputAmount, uint256 inputReserve, uint256 outputReserve) 206 | internal 207 | pure 208 | returns (uint256) 209 | { 210 | uint256 numerator = inputReserve.mul(outputAmount).mul(1000); 211 | uint256 denominator = outputReserve.sub(outputAmount).mul(995); 212 | return numerator.div(denominator).add(1); 213 | } 214 | 215 | // ***** Trade Ether to Tokens 216 | // ------------------- 217 | 218 | /// @dev Exact amount of ether -> As many tokens as can be bought, without partial refund 219 | function ethToTokenInput( 220 | uint256 ethSold, 221 | uint256 minTokens, 222 | uint256 deadline, 223 | address recipient 224 | ) 225 | internal 226 | returns (uint256) 227 | { 228 | require(deadline >= now); // dev: deadline passed 229 | require(ethSold != 0); // dev: no eth to sell 230 | require(minTokens != 0); // dev: must buy one or more tokens 231 | uint256 ownedSupply = _ownedSupply; 232 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + ownedSupply); 233 | uint256 ethReserve = address(this).balance.sub(ethSold); 234 | uint256 tokensBought = getInputPrice(ethSold, ethReserve, tokenReserve); 235 | require(tokensBought >= minTokens); // dev: not enough eth to buy tokens 236 | _balances[recipient] += tokensBought; 237 | _ownedSupply = ownedSupply + tokensBought; 238 | return tokensBought; 239 | } 240 | 241 | /// @notice Convert ETH to Tokens 242 | /// @dev User cannot specify minimum output or deadline. 243 | receive() external payable { 244 | ethToTokenInput(msg.value, 1, now, msg.sender); 245 | } 246 | 247 | /// @notice Convert ether to tokens. Specify the exact input (in ether) and 248 | /// the minimum output (in tokens). 249 | /// @param minTokens The minimum amount of tokens you want to receive in the 250 | /// transaction for your sold ether. Will revert if less than `minTokens` 251 | /// tokens would be transferred. 252 | /// @param deadline The time after which the transaction can no longer be executed. 253 | /// Will revert if the current timestamp is after the deadline. 254 | /// @dev Excess ether for buying a partial token is not refunded. 255 | /// @return The amount of tokens bought. 256 | function ethToTokenSwapInput(uint256 minTokens, uint256 deadline) 257 | external 258 | payable 259 | returns (uint256) 260 | { 261 | return ethToTokenInput(msg.value, minTokens, deadline, msg.sender); 262 | } 263 | 264 | /// @notice Convert ether to tokens and transfer tokens to `recipient`. 265 | /// Specify the exact input (in ether) and the minimum output (in tokens). 266 | /// @param minTokens The minimum amount of tokens you want the `recipient` to 267 | /// receive in the transaction for your sold ether. 268 | /// Will revert if less than `minTokens` tokens would be transferred. 269 | /// @param deadline The time after which the transaction can no longer be executed. 270 | /// Will revert if the current timestamp is after the deadline. 271 | /// @param recipient Bought tokens will be transferred to this address. 272 | /// @dev Excess ether for buying a partial token is not refunded. 273 | /// Requirements: 274 | /// - `recipient` can't be this contract or the zero address 275 | /// @return The amount of tokens bought and transferred to `recipient`. 276 | function ethToTokenTransferInput(uint256 minTokens, uint256 deadline, address recipient) 277 | external 278 | payable 279 | returns (uint256) 280 | { 281 | require(recipient != address(this)); // dev: can't send to liquid token contract 282 | require(recipient != address(0)); // dev: can't send to zero address 283 | return ethToTokenInput(msg.value, minTokens, deadline, recipient); 284 | } 285 | 286 | 287 | /// @dev Any amount of ether (at least cost of tokens) -> Exact amount of tokens + refund 288 | function ethToTokenOutput( 289 | uint256 tokensBought, 290 | uint256 maxEth, 291 | uint256 deadline, 292 | address payable buyer, 293 | address recipient 294 | ) 295 | internal 296 | returns (uint256) 297 | { 298 | require(deadline >= now); // dev: deadline passed 299 | require(tokensBought != 0); // dev: must buy one or more tokens 300 | require(maxEth != 0); // dev: maxEth must greater than 0 301 | uint256 ownedSupply = _ownedSupply; 302 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + ownedSupply); 303 | uint256 ethReserve = address(this).balance.sub(maxEth); 304 | uint256 ethSold = getOutputPrice(tokensBought, ethReserve, tokenReserve); 305 | uint256 ethRefund = maxEth.sub(ethSold, "LGT: not enough ETH"); 306 | _balances[recipient] += tokensBought; 307 | _ownedSupply = ownedSupply + tokensBought; 308 | if (ethRefund != 0) { 309 | buyer.call{value: ethRefund}(""); 310 | } 311 | return ethSold; 312 | } 313 | 314 | /// @notice Convert ether to tokens. Specify the maximum input (in ether) and 315 | /// the exact output (in tokens). Any remaining ether is refunded. 316 | /// @param tokensBought The exact amount of tokens you want to receive. 317 | /// Will revert if less than `tokensBought` tokens can be bought 318 | /// with the sent amount of ether. 319 | /// @param deadline The time after which the transaction can no longer be executed. 320 | /// Will revert if the current timestamp is after the deadline. 321 | /// @dev Excess ether after buying `tokensBought` tokens is refunded. 322 | /// @return The amount of ether sold to buy `tokensBought` tokens. 323 | function ethToTokenSwapOutput(uint256 tokensBought, uint256 deadline) 324 | external 325 | payable 326 | returns (uint256) 327 | { 328 | return ethToTokenOutput(tokensBought, msg.value, deadline, msg.sender, msg.sender); 329 | } 330 | 331 | /// @notice Convert ether to tokens and transfer tokens to `recipient`. 332 | /// Specify the maximum input (in ether) and the exact output (in tokens). 333 | /// Any remaining ether is refunded. 334 | /// @param tokensBought The exact amount of tokens you want to buy and transfer to 335 | /// `recipient`. Will revert if less than `tokensBought` tokens can be bought 336 | /// with the sent amount of ether. 337 | /// @param deadline The time after which the transaction can no longer be executed. 338 | /// Will revert if the current timestamp is after the deadline. 339 | /// @param recipient Bought tokens will be transferred to this address. 340 | /// @dev Excess ether for buying a partial token is not refunded. 341 | /// Requirements: 342 | /// - `recipient` can't be this contract or the zero address 343 | /// @return The amount of ether sold to buy `tokensBought` tokens. 344 | function ethToTokenTransferOutput(uint256 tokensBought, uint256 deadline, address recipient) 345 | external 346 | payable 347 | returns (uint256) 348 | { 349 | require(recipient != address(this)); // dev: can't send to liquid token contract 350 | require(recipient != address(0)); // dev: can't send to zero address 351 | return ethToTokenOutput(tokensBought, msg.value, deadline, msg.sender, recipient); 352 | } 353 | 354 | 355 | // ***** Trade Tokens to Ether 356 | // --------------------- 357 | 358 | /// @dev Exact amount of tokens -> Minimum amount of ether 359 | function tokenToEthInput( 360 | uint256 tokensSold, 361 | uint256 minEth, 362 | uint256 deadline, 363 | address buyer, 364 | address payable recipient 365 | ) internal returns (uint256) { 366 | require(deadline >= now); // dev: deadline passed 367 | require(tokensSold != 0); // dev: must sell one or more tokens 368 | require(minEth != 0); // dev: minEth not set 369 | uint256 ownedSupply = _ownedSupply; 370 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + ownedSupply); 371 | uint256 ethBought = getInputPrice(tokensSold, tokenReserve, address(this).balance); 372 | require(ethBought >= minEth); // dev: tokens not worth enough 373 | _balances[buyer] = _balances[buyer].sub(tokensSold, "LGT: amount exceeds balance"); 374 | _ownedSupply = ownedSupply.sub(tokensSold); 375 | recipient.call{value: ethBought}(""); 376 | return ethBought; 377 | } 378 | 379 | /// @dev Transferring tokens to this contract will sell them. 380 | /// User cannot specify minEth or deadline. 381 | function _transferToSelf(address sender, uint256 amount) internal override { 382 | address payable _sender = payable(sender); 383 | tokenToEthInput(amount, 1, now, _sender, _sender); 384 | } 385 | 386 | /// @notice Convert tokens to ether. Specify the exact input (in tokens) and 387 | /// the minimum output (in ether). 388 | /// @param tokensSold The exact amount of tokens you want to sell in the 389 | /// transaction. Will revert you own less than `minTokens` tokens. 390 | /// @param minEth The minimum amount of ether you want to receive for the sale 391 | /// of `tokensSold` tokens. Will revert if less ether would be received. 392 | /// @param deadline The time after which the transaction can no longer be executed. 393 | /// Will revert if the current timestamp is after the deadline. 394 | /// @return The amount of ether bought. 395 | function tokenToEthSwapInput(uint256 tokensSold, uint256 minEth, uint256 deadline) 396 | external 397 | returns (uint256) 398 | { 399 | return tokenToEthInput(tokensSold, minEth, deadline, msg.sender, msg.sender); 400 | } 401 | 402 | /// @notice Convert tokens to ether and transfer it to `recipient`. 403 | /// Specify the exact input (in tokens) and the minimum output (in ether). 404 | /// @param tokensSold The exact amount of tokens you want to sell in the 405 | /// transaction. Will revert you own less than `minTokens` tokens. 406 | /// @param minEth The minimum amount of ether you want the `recipient` to receive for 407 | /// the sale of `tokensSold` tokens. Will revert if less ether would be transferred. 408 | /// @param deadline The time after which the transaction can no longer be executed. 409 | /// Will revert if the current timestamp is after the deadline. 410 | /// @param recipient Bought ether will be transferred to this address. 411 | /// @dev Requirements: 412 | /// - `recipient` can't be this contract or the zero address 413 | /// @return The amount of ether bought. 414 | function tokenToEthTransferInput( 415 | uint256 tokensSold, 416 | uint256 minEth, 417 | uint256 deadline, 418 | address payable recipient 419 | ) external returns (uint256) { 420 | require(recipient != address(this)); // dev: can't send to liquid token contract 421 | require(recipient != address(0)); // dev: can't send to zero address 422 | return tokenToEthInput(tokensSold, minEth, deadline, msg.sender, recipient); 423 | } 424 | 425 | 426 | /// @dev Maximum amount of tokens -> Exact amount of ether 427 | function tokenToEthOutput( 428 | uint256 ethBought, 429 | uint256 maxTokens, 430 | uint256 deadline, 431 | address buyer, 432 | address payable recipient 433 | ) internal returns (uint256) { 434 | require(deadline >= now); // dev: deadline passed 435 | require(ethBought != 0); // dev: must buy more than 0 eth 436 | uint256 ownedSupply = _ownedSupply; 437 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + ownedSupply); 438 | uint256 tokensSold = getOutputPrice(ethBought, tokenReserve, address(this).balance); 439 | require(maxTokens >= tokensSold); // dev: need more tokens to sell 440 | _balances[buyer] = _balances[buyer].sub(tokensSold, "LGT: amount exceeds balance"); 441 | _ownedSupply = ownedSupply.sub(tokensSold); 442 | recipient.call{value: ethBought}(""); 443 | return tokensSold; 444 | } 445 | 446 | /// @notice Convert tokens to ether. Specify the maximum input (in tokens) and 447 | /// the exact output (in ether). 448 | /// @param ethBought The exact amount of ether you want to receive in the 449 | /// transaction. Will revert if tokens can't be sold for enough ether. 450 | /// @param maxTokens The maximum amount of tokens you are willing to sell to 451 | /// receive `ethBought` ether. Will revert if more tokens would be needed. 452 | /// @param deadline The time after which the transaction can no longer be executed. 453 | /// Will revert if the current timestamp is after the deadline. 454 | /// @return The amount of tokens sold. 455 | function tokenToEthSwapOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline) 456 | external 457 | returns (uint256) 458 | { 459 | return tokenToEthOutput(ethBought, maxTokens, deadline, msg.sender, msg.sender); 460 | } 461 | 462 | /// @notice Convert tokens to ether and transfer it to `recipient`. 463 | /// Specify the maximum input (in tokens) and the exact output (in ether). 464 | /// @param ethBought The exact amount of ether you want `recipient` to receive in the 465 | /// transaction. Will revert if tokens can't be sold for enough ether. 466 | /// @param maxTokens The maximum amount of tokens you are willing to sell to 467 | /// buy `ethBought` ether. Will revert if more tokens would be needed. 468 | /// @param deadline The time after which the transaction can no longer be executed. 469 | /// Will revert if the current timestamp is after the deadline. 470 | /// @param recipient Bought ether will be transferred to this address. 471 | /// @dev Requirements: 472 | /// - `recipient` can't be this contract or the zero address 473 | /// @return The amount of tokens sold. 474 | function tokenToEthTransferOutput( 475 | uint256 ethBought, 476 | uint256 maxTokens, 477 | uint256 deadline, 478 | address payable recipient 479 | ) 480 | external 481 | returns (uint256) 482 | { 483 | require(recipient != address(this)); // dev: can't send to liquid token contract 484 | require(recipient != address(0)); // dev: can't send to zero address 485 | return tokenToEthOutput(ethBought, maxTokens, deadline, msg.sender, recipient); 486 | } 487 | 488 | // ***** Public Price Functions 489 | // -------------------- 490 | 491 | /// @notice How many tokens can I buy with `ethSold` ether? 492 | /// @param ethSold The exact amount of ether you are selling. 493 | /// @return The amount of tokens that can be bought with `ethSold` ether. 494 | function getEthToTokenInputPrice(uint256 ethSold) public view returns(uint256) { 495 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + _ownedSupply); 496 | return getInputPrice(ethSold, address(this).balance, tokenReserve); 497 | } 498 | 499 | /// @notice What is the price for `tokensBought` tokens? 500 | /// @param tokensBought The exact amount of tokens bought 501 | /// @return The amount of ether needed to buy `tokensBought` tokens 502 | function getEthToTokenOutputPrice(uint256 tokensBought) public view returns (uint256) { 503 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + _ownedSupply); 504 | return getOutputPrice(tokensBought, address(this).balance, tokenReserve); 505 | } 506 | 507 | /// @notice How much ether do I receive when selling `tokensSold` tokens? 508 | /// @param tokensSold The exact amount of tokens you are selling. 509 | /// @return The amount of ether you receive for selling `tokensSold` tokens. 510 | function getTokenToEthInputPrice(uint256 tokensSold) public view returns (uint256) { 511 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + _ownedSupply); 512 | return getInputPrice(tokensSold, tokenReserve, address(this).balance); 513 | } 514 | 515 | /// @notice How many tokens do I need to sell to receive `ethBought` ether? 516 | /// @param ethBought The exact amount of ether you are buying. 517 | /// @return The amount of tokens needed to buy `ethBought` ether. 518 | function getTokenToEthOutputPrice(uint256 ethBought) public view returns (uint256) { 519 | uint256 tokenReserve = _totalMinted.sub(_totalBurned + _ownedSupply); 520 | return getOutputPrice(ethBought, tokenReserve, address(this).balance); 521 | } 522 | } 523 | --------------------------------------------------------------------------------