From 21d2519ec32dae584270e040269a55a30f1be02e Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Fri, 7 Apr 2023 15:44:27 -0500 Subject: [PATCH 01/16] Changer whale addresses and add logs to tests --- src/BitSignal.sol | 3 +++ test/BitSignal.t.sol | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index d13cdf3..f420ed1 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; +import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol'; +import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; + interface ERC20 { function approve(address spender, uint256 amount) external; function transfer(address recipient, uint256 amount) external; diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index e16b7f6..df9ce65 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; + import {BitSignal, ERC20} from "../src/BitSignal.sol"; contract BigSignalTest is Test { @@ -10,8 +11,8 @@ contract BigSignalTest is Test { ERC20 constant WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals address public balajis = address(0x1); address public counterparty = address(0x2); - address usdcWhale = 0xF977814e90dA44bFA03b6295A0616a897441aceC; // arbitrary holder addresses chosen to seed our tests - address wbtcWhale = 0x6daB3bCbFb336b29d06B9C793AEF7eaA57888922; + address usdcWhale = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; // arbitrary holder addresses chosen to seed our tests + address wbtcWhale = 0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656; function setUp() public { bitsignal = new BitSignal(balajis, counterparty); @@ -23,11 +24,19 @@ contract BigSignalTest is Test { } function testDepositAndInitiateBetAndSettle() public { + console2.logString('testDepositAndInitiateBetAndSettle beginning...'); + console2.logString('USDC whale balance:'); + uint256 usdsWhaleBalance = USDC.balanceOf(usdcWhale); + console2.logUint(usdsWhaleBalance); + console2.log('USDC whale balance: %d', usdsWhaleBalance); // Fund balajis and counterparty for test prep vm.prank(usdcWhale); USDC.transfer(balajis, 1_000_000e6); + console2.logString('USDC been transferred'); vm.prank(wbtcWhale); WBTC.transfer(counterparty, 1e8); + console.logString('WBTC been transferred'); + // Now simulate the deposits vm.startPrank(balajis); From 5fa95267f7ba42801da8fc471a94092e3245aa90 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Fri, 7 Apr 2023 15:44:46 -0500 Subject: [PATCH 02/16] forge install: v3-periphery v1.3.0 --- .gitmodules | 3 +++ lib/v3-periphery | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/v3-periphery diff --git a/.gitmodules b/.gitmodules index 80b9773..3499405 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = lib/forge-std url = https://github.com/foundry-rs/forge-std branch = v1.5.2 +[submodule "lib/v3-periphery"] + path = lib/v3-periphery + url = https://github.com/Uniswap/v3-periphery diff --git a/lib/v3-periphery b/lib/v3-periphery new file mode 160000 index 0000000..80f26c8 --- /dev/null +++ b/lib/v3-periphery @@ -0,0 +1 @@ +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 From 921be682a033fa6477906a81981cc971576a49ab Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Fri, 7 Apr 2023 15:46:37 -0500 Subject: [PATCH 03/16] forge install: v3-core v1.0.0 --- .gitmodules | 3 +++ lib/v3-core | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/v3-core diff --git a/.gitmodules b/.gitmodules index 3499405..440702b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "lib/v3-periphery"] path = lib/v3-periphery url = https://github.com/Uniswap/v3-periphery +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/Uniswap/v3-core diff --git a/lib/v3-core b/lib/v3-core new file mode 160000 index 0000000..e3589b1 --- /dev/null +++ b/lib/v3-core @@ -0,0 +1 @@ +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 From 78267b76ee915387fdff7d27027e81e5aa78f2a4 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Fri, 7 Apr 2023 15:47:06 -0500 Subject: [PATCH 04/16] forge install: openzeppelin-contracts v4.8.2 --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 440702b..679065d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ [submodule "lib/v3-core"] path = lib/v3-core url = https://github.com/Uniswap/v3-core +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..d00acef --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit d00acef4059807535af0bd0dd0ddf619747a044b From 5ec8aa7adb63c88d0575aeef1ab763ef51445cf5 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Fri, 7 Apr 2023 18:09:34 -0500 Subject: [PATCH 05/16] Add swapCollateral --- foundry.toml | 3 +- src/BitSignal.sol | 68 +++++++++++++++++++++++++++++++++++--------- test/BitSignal.t.sol | 18 +++++++++--- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/foundry.toml b/foundry.toml index e6810b2..3a32944 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,6 @@ src = 'src' out = 'out' libs = ['lib'] +remappings=['@uniswap/v3-periphery/=lib/v3-periphery/', '@uniswap/v3-core/=lib/v3-core/', '@openzeppelin/=lib/openzeppelin-contracts/'] -# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/src/BitSignal.sol b/src/BitSignal.sol index f420ed1..b8eee2a 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -3,13 +3,9 @@ pragma solidity ^0.8.19; import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol'; import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; -interface ERC20 { - function approve(address spender, uint256 amount) external; - function transfer(address recipient, uint256 amount) external; - function transferFrom(address sender, address recipient, uint256 amount) external; - function balanceOf(address holder) external returns (uint256); -} +import "forge-std/console2.sol"; interface AggregatorV3Interface { function decimals() external view returns (uint8); @@ -26,17 +22,21 @@ interface AggregatorV3Interface { ); } -contract BitSignal { +contract BitSignal is Ownable { uint256 constant BET_LENGTH = 90 days; uint256 constant PRICE_THRESHOLD = 1_000_000; // 1 million USD per BTC uint256 constant USDC_AMOUNT = 1_000_000e6; uint256 constant WBTC_AMOUNT = 1e8; + uint256 constant STABLECOIN_MIN_PRICE = 97000000; // if price drops below 97 cents consider it as a depeg and permit swap - ERC20 constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals - ERC20 constant WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals + address constant UNISWAP_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + IERC20 constant USDC = IERC20(USDC_ADDRESS); // 6 decimals + IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals - AggregatorV3Interface priceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals + AggregatorV3Interface btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals + AggregatorV3Interface usdcPriceFeed = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); // 8 decimals address public immutable balajis; address public immutable counterparty; @@ -47,11 +47,53 @@ contract BitSignal { uint256 public startTimestamp; - constructor(address _balajis, address _counterparty) { + address[] STABLECOIN_CONTRACTS = [ + USDC_ADDRESS, // Circle USDC + 0xdAC17F958D2ee523a2206206994597C13D831ec7, // Tether USDT + 0x4Fabb145d64652a948d72533023f6E7A623C7C53, // Binance BUSD + 0x8E870D67F660D95d5be530380D0eC0bd388289E1 // Paxos USDP + ]; + + modifier swapAllowed(address token) { + // this function will be used only in case of emergency + // that`s why its better to consume more gas here due to search in array + // rather than building map in constructor + uint256 usdcPrice = chainlinkPrice(usdcPriceFeed); + console2.log(usdcPrice); + require(usdcPrice <= STABLECOIN_MIN_PRICE, "Collateral coin haven`t lost its peg"); + bool found; + for (uint i=0; i<4; i++) { + if (STABLECOIN_CONTRACTS[i] == token) { + found = true; + } + } + require(found, "swap to choosen token is not allowed"); + _; + } + + constructor(address _balajis, address _counterparty) Ownable() { balajis = _balajis; counterparty = _counterparty; } + /// @notice Let arbitor to swap collateral in case deposited stablecoin starts to loose it's peg + function swapCollateral(address token, uint256 amountMinimum, uint24 poolFee) external onlyOwner swapAllowed(token) returns (uint256) { + ISwapRouter swapRouter = ISwapRouter(UNISWAP_ROUTER); + TransferHelper.safeApprove(USDC_ADDRESS, UNISWAP_ROUTER, USDC_AMOUNT); + ISwapRouter.ExactInputSingleParams memory params = + ISwapRouter.ExactInputSingleParams({ + tokenIn: USDC_ADDRESS, + tokenOut: token, + fee: poolFee, + recipient: address(this), + deadline: block.timestamp, + amountIn: USDC_AMOUNT, + amountOutMinimum: amountMinimum, + sqrtPriceLimitX96: 0 + }); + return swapRouter.exactInputSingle(params); + } + /// @notice Deposit USDC collateral. This initiates the bet if WBTC already deposited function depositUSDC() external { require(msg.sender == balajis && !usdcDeposited, "unauthorized"); @@ -98,7 +140,7 @@ contract BitSignal { betInitiated = false; - uint256 wbtcPrice = chainlinkPrice() / 10**priceFeed.decimals(); + uint256 wbtcPrice = chainlinkPrice(btcPriceFeed) / 10**btcPriceFeed.decimals(); address winner; if (wbtcPrice >= PRICE_THRESHOLD) { @@ -112,7 +154,7 @@ contract BitSignal { } /// @notice Fetch the BTCUSD price with 8 decimals included - function chainlinkPrice() public view returns (uint256) { + function chainlinkPrice(AggregatorV3Interface priceFeed) public view returns (uint256) { ( /* uint80 roundID */, int price, diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index df9ce65..e566ad5 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -3,23 +3,28 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {BitSignal, ERC20} from "../src/BitSignal.sol"; +import "forge-std/interfaces/IERC20.sol"; +import {BitSignal, AggregatorV3Interface} from "../src/BitSignal.sol"; contract BigSignalTest is Test { BitSignal public bitsignal; - ERC20 constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals - ERC20 constant WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals + IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals + IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals address public balajis = address(0x1); address public counterparty = address(0x2); + address public arbitor = address(0x3); address usdcWhale = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; // arbitrary holder addresses chosen to seed our tests address wbtcWhale = 0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656; + address Tether_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; function setUp() public { + vm.prank(arbitor); bitsignal = new BitSignal(balajis, counterparty); } function testSettle() public { - uint256 price = bitsignal.chainlinkPrice() / 1e8; + AggregatorV3Interface btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals + uint256 price = bitsignal.chainlinkPrice(btcPriceFeed) / 1e8; console2.log(price); } @@ -85,4 +90,9 @@ contract BigSignalTest is Test { vm.stopPrank(); } + function testSwap() public { + vm.prank(arbitor); + bitsignal.swapCollateral(Tether_USDT, 0, 3000); + } + } From eebc5210f0b8c983c1b7717a452eb858d6e45c57 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sat, 8 Apr 2023 19:27:59 -0500 Subject: [PATCH 06/16] Fixed swap function and added test --- script/Deploy.s.sol | 2 +- src/BitSignal.sol | 50 +++++++++++++++++++++++++++----------------- test/BitSignal.t.sol | 43 ++++++++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index a1dc166..a1425a7 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -11,7 +11,7 @@ contract BitSignalScript is Script { function run() public { vm.startBroadcast(); - BitSignal bitsignal = new BitSignal(address(0x1), address(0x2)); + BitSignal bitsignal = new BitSignal(address(0x1), address(0x2), 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); vm.stopBroadcast(); diff --git a/src/BitSignal.sol b/src/BitSignal.sol index b8eee2a..16cbbec 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -30,14 +30,15 @@ contract BitSignal is Ownable { uint256 constant WBTC_AMOUNT = 1e8; uint256 constant STABLECOIN_MIN_PRICE = 97000000; // if price drops below 97 cents consider it as a depeg and permit swap - address constant UNISWAP_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - IERC20 constant USDC = IERC20(USDC_ADDRESS); // 6 decimals + address constant UNISWAP_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address constant WETH_CONTRACT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals - AggregatorV3Interface btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals - AggregatorV3Interface usdcPriceFeed = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); // 8 decimals + AggregatorV3Interface immutable btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals + AggregatorV3Interface immutable usdcPriceFeed; + IERC20 collateral = USDC; address public immutable balajis; address public immutable counterparty; @@ -48,7 +49,7 @@ contract BitSignal is Ownable { uint256 public startTimestamp; address[] STABLECOIN_CONTRACTS = [ - USDC_ADDRESS, // Circle USDC + address(USDC), // Circle USDC 0xdAC17F958D2ee523a2206206994597C13D831ec7, // Tether USDT 0x4Fabb145d64652a948d72533023f6E7A623C7C53, // Binance BUSD 0x8E870D67F660D95d5be530380D0eC0bd388289E1 // Paxos USDP @@ -58,6 +59,7 @@ contract BitSignal is Ownable { // this function will be used only in case of emergency // that`s why its better to consume more gas here due to search in array // rather than building map in constructor + require(betInitiated, "bet not initiated"); uint256 usdcPrice = chainlinkPrice(usdcPriceFeed); console2.log(usdcPrice); require(usdcPrice <= STABLECOIN_MIN_PRICE, "Collateral coin haven`t lost its peg"); @@ -71,27 +73,33 @@ contract BitSignal is Ownable { _; } - constructor(address _balajis, address _counterparty) Ownable() { + constructor(address _balajis, address _counterparty, address _usdcPriceFeedAddress) Ownable() { balajis = _balajis; counterparty = _counterparty; + usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeedAddress); // 8 decimals } /// @notice Let arbitor to swap collateral in case deposited stablecoin starts to loose it's peg - function swapCollateral(address token, uint256 amountMinimum, uint24 poolFee) external onlyOwner swapAllowed(token) returns (uint256) { + function swapCollateral(address token, uint256 amountMinimum, uint24 feeToWeth, uint24 feeFromWeth) external onlyOwner swapAllowed(token) returns (uint256) { ISwapRouter swapRouter = ISwapRouter(UNISWAP_ROUTER); - TransferHelper.safeApprove(USDC_ADDRESS, UNISWAP_ROUTER, USDC_AMOUNT); - ISwapRouter.ExactInputSingleParams memory params = - ISwapRouter.ExactInputSingleParams({ - tokenIn: USDC_ADDRESS, - tokenOut: token, - fee: poolFee, + TransferHelper.safeApprove(address(USDC), UNISWAP_ROUTER, USDC_AMOUNT); + ISwapRouter.ExactInputParams memory params = + ISwapRouter.ExactInputParams({ + path: abi.encodePacked( + address(USDC), + feeToWeth, + WETH_CONTRACT, + feeFromWeth, + token + ), recipient: address(this), deadline: block.timestamp, amountIn: USDC_AMOUNT, - amountOutMinimum: amountMinimum, - sqrtPriceLimitX96: 0 + amountOutMinimum: amountMinimum }); - return swapRouter.exactInputSingle(params); + uint256 output = swapRouter.exactInput(params); + collateral = IERC20(token); + return output; } /// @notice Deposit USDC collateral. This initiates the bet if WBTC already deposited @@ -149,11 +157,15 @@ contract BitSignal is Ownable { winner = counterparty; } - USDC.transfer(winner, USDC.balanceOf(address(this))); + collateral.transfer(winner, collateral.balanceOf(address(this))); WBTC.transfer(winner, WBTC.balanceOf(address(this))); + // in case there wasn't enough liquidity in Uniswap pool and some USDC change left + if (address(collateral) != address(USDC)) { + USDC.transfer(winner, USDC.balanceOf(address(this))); + } } - /// @notice Fetch the BTCUSD price with 8 decimals included + /// @notice Fetch the token price with 8 decimals included function chainlinkPrice(AggregatorV3Interface priceFeed) public view returns (uint256) { ( /* uint80 roundID */, diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index e566ad5..41a40c1 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -5,21 +5,26 @@ import "forge-std/Test.sol"; import "forge-std/interfaces/IERC20.sol"; import {BitSignal, AggregatorV3Interface} from "../src/BitSignal.sol"; +import {MockPriceFeed} from "./MockPriceFeed.sol"; contract BigSignalTest is Test { BitSignal public bitsignal; + MockPriceFeed public mockUsdcPriceFeed; IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals + IERC20 constant USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); + IERC20 constant WETH9 = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); address public balajis = address(0x1); address public counterparty = address(0x2); address public arbitor = address(0x3); address usdcWhale = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; // arbitrary holder addresses chosen to seed our tests address wbtcWhale = 0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656; - address Tether_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address USDC_PRICE_FEED_ADDRESS = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; function setUp() public { + mockUsdcPriceFeed = new MockPriceFeed(); vm.prank(arbitor); - bitsignal = new BitSignal(balajis, counterparty); + bitsignal = new BitSignal(balajis, counterparty, address(mockUsdcPriceFeed)); } function testSettle() public { @@ -91,8 +96,40 @@ contract BigSignalTest is Test { } function testSwap() public { + // Fund balajis and counterparty for test prep + vm.prank(usdcWhale); + USDC.transfer(balajis, 1_000_000e6); + vm.prank(wbtcWhale); + WBTC.transfer(counterparty, 1e8); + + + vm.startPrank(balajis); + USDC.approve(address(bitsignal), type(uint256).max); + bitsignal.depositUSDC(); + vm.stopPrank(); + + vm.startPrank(counterparty); + WBTC.approve(address(bitsignal), type(uint256).max); + bitsignal.depositWBTC(); + vm.stopPrank(); + + mockUsdcPriceFeed.setAnswer(95000000); + ( + /* uint80 roundID */, + int price, + /*uint startedAt*/, + /*uint timeStamp*/, + /*uint80 answeredInRound*/ + ) = mockUsdcPriceFeed.latestRoundData(); + + console2.log('Decimals: %d', mockUsdcPriceFeed.decimals()); + console2.log('Mocked price: %d', price); vm.prank(arbitor); - bitsignal.swapCollateral(Tether_USDT, 0, 3000); + uint256 output = bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + + console2.log("Output: %d", output); + console2.log("USDC balance after swap: %d", USDC.balanceOf(address(bitsignal))); + console2.log("USDT balance after swap: %d", USDT.balanceOf(address(bitsignal))); } } From 128e82fabc752decf2f420f2a8611f1b4ceba1d8 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sat, 8 Apr 2023 19:28:25 -0500 Subject: [PATCH 07/16] Added mock for Chainlink --- test/MockPriceFeed.sol | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/MockPriceFeed.sol diff --git a/test/MockPriceFeed.sol b/test/MockPriceFeed.sol new file mode 100644 index 0000000..0d1b30c --- /dev/null +++ b/test/MockPriceFeed.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {AggregatorV3Interface} from '../src/BitSignal.sol'; + +contract MockPriceFeed is AggregatorV3Interface { + + int256 internal price; + + function setAnswer(int256 _answer) external { + price = _answer; + } + + function decimals() external view returns (uint8) { + return 8; + } + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + return (0, price, 0, 0, 0); + } +} From 347434aa81b19e6c4b2eda9b014ce5bece3e4855 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sat, 8 Apr 2023 20:44:16 -0500 Subject: [PATCH 08/16] Add additional tests for swap and fix error with token transfer --- src/BitSignal.sol | 13 +++++++-- test/BitSignal.t.sol | 66 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index 16cbbec..4a2b3e8 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol'; import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; import '@openzeppelin/contracts/access/Ownable.sol'; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "forge-std/console2.sol"; @@ -59,9 +60,8 @@ contract BitSignal is Ownable { // this function will be used only in case of emergency // that`s why its better to consume more gas here due to search in array // rather than building map in constructor - require(betInitiated, "bet not initiated"); + require(betInitiated, "bet is not initiated"); uint256 usdcPrice = chainlinkPrice(usdcPriceFeed); - console2.log(usdcPrice); require(usdcPrice <= STABLECOIN_MIN_PRICE, "Collateral coin haven`t lost its peg"); bool found; for (uint i=0; i<4; i++) { @@ -157,10 +157,17 @@ contract BitSignal is Ownable { winner = counterparty; } - collateral.transfer(winner, collateral.balanceOf(address(this))); + console2.log('Winner choosen %s', winner); + console2.log('Collateral address: %s', address(collateral)); + console2.log('Collateral balance: %d', collateral.balanceOf(address(this))); + + SafeERC20.safeTransfer(collateral, winner, collateral.balanceOf(address(this))); + //collateral.transfer(winner, collateral.balanceOf(address(this))); + console2.log('Collateral transferred'); WBTC.transfer(winner, WBTC.balanceOf(address(this))); // in case there wasn't enough liquidity in Uniswap pool and some USDC change left if (address(collateral) != address(USDC)) { + console2.log('Before change transfer'); USDC.transfer(winner, USDC.balanceOf(address(this))); } } diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index 41a40c1..92c2910 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -14,9 +14,9 @@ contract BigSignalTest is Test { IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals IERC20 constant USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); IERC20 constant WETH9 = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - address public balajis = address(0x1); - address public counterparty = address(0x2); - address public arbitor = address(0x3); + address public balajis = address(0x53Cfaa403a214c9be35011B3Dcfb75D81D2F7B6B); + address public counterparty = address(0x83d47D101881A1E52Ae9C6A2272f499601b8fBCF); + address public arbitor = address(0xc37B6361aff0A159Ebc4926285C9DDc8a9D0d1bA); address usdcWhale = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; // arbitrary holder addresses chosen to seed our tests address wbtcWhale = 0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656; address USDC_PRICE_FEED_ADDRESS = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; @@ -108,28 +108,64 @@ contract BigSignalTest is Test { bitsignal.depositUSDC(); vm.stopPrank(); + vm.prank(arbitor); + vm.expectRevert("bet is not initiated"); + bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + vm.startPrank(counterparty); WBTC.approve(address(bitsignal), type(uint256).max); bitsignal.depositWBTC(); vm.stopPrank(); + mockUsdcPriceFeed.setAnswer(99000000); + + vm.prank(arbitor); + vm.expectRevert("Collateral coin haven`t lost its peg"); + bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + + mockUsdcPriceFeed.setAnswer(95000000); - ( - /* uint80 roundID */, - int price, - /*uint startedAt*/, - /*uint timeStamp*/, - /*uint80 answeredInRound*/ - ) = mockUsdcPriceFeed.latestRoundData(); - - console2.log('Decimals: %d', mockUsdcPriceFeed.decimals()); - console2.log('Mocked price: %d', price); + vm.prank(balajis); + vm.expectRevert("Ownable: caller is not the owner"); + bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + + vm.prank(arbitor); + vm.expectRevert("swap to choosen token is not allowed"); + bitsignal.swapCollateral(address(WETH9), 900_000e6, 500, 3000); + vm.prank(arbitor); - uint256 output = bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + uint256 swapOutput = bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); - console2.log("Output: %d", output); + console2.log("SwapCollateral output: %d", swapOutput); console2.log("USDC balance after swap: %d", USDC.balanceOf(address(bitsignal))); console2.log("USDT balance after swap: %d", USDT.balanceOf(address(bitsignal))); + console2.log("balajis address: %s", balajis); + console2.log("counterparty address: %s", counterparty); + + + console2.log(bitsignal.betInitiated()); + + uint256 usdtBeforeSettlement = USDT.balanceOf(counterparty); + uint256 wbtcBeforeSettlement = WBTC.balanceOf(counterparty); + + // Successfully settle after expiry + vm.warp(block.timestamp + 100 days); + bitsignal.settle(); + + // Check that winnings received + assertEq(USDT.balanceOf(counterparty), usdtBeforeSettlement + swapOutput); + assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); } } + + +// test balajis wins the bet +// test counterparty wins the bet +// test swap is not allowed to anyone other than arbitor +// test swap is not allowed before bet is initiated +// test swap is not allowed if USDC haven't lost is peg +// test winner recieves alternatiwe stablecoin after swap +// test winner receives a change if not all amount of USDC been swapped +// test on fork and on mainner for price feed + From 707cf7cf2279f262bf7243422dcb781dda65c430 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sun, 9 Apr 2023 09:50:47 -0500 Subject: [PATCH 09/16] Add priceFeed for BTC and test different winners scenarious --- script/Deploy.s.sol | 7 ++- src/BitSignal.sol | 5 +- test/BitSignal.t.sol | 135 +++++++++++++++++++++++++---------------- test/MockPriceFeed.sol | 7 ++- 4 files changed, 97 insertions(+), 57 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index a1425a7..e081acf 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -11,7 +11,12 @@ contract BitSignalScript is Script { function run() public { vm.startBroadcast(); - BitSignal bitsignal = new BitSignal(address(0x1), address(0x2), 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); + BitSignal bitsignal = new BitSignal( + address(0x1), + address(0x2), + 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6, + 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c + ); vm.stopBroadcast(); diff --git a/src/BitSignal.sol b/src/BitSignal.sol index 4a2b3e8..baa4347 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -36,7 +36,7 @@ contract BitSignal is Ownable { IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals - AggregatorV3Interface immutable btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals + AggregatorV3Interface immutable btcPriceFeed; AggregatorV3Interface immutable usdcPriceFeed; IERC20 collateral = USDC; @@ -73,10 +73,11 @@ contract BitSignal is Ownable { _; } - constructor(address _balajis, address _counterparty, address _usdcPriceFeedAddress) Ownable() { + constructor(address _balajis, address _counterparty, address _usdcPriceFeedAddress, address _btcPriceFeedAddress) Ownable() { balajis = _balajis; counterparty = _counterparty; usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeedAddress); // 8 decimals + btcPriceFeed = AggregatorV3Interface(_btcPriceFeedAddress); // 8 decimals } /// @notice Let arbitor to swap collateral in case deposited stablecoin starts to loose it's peg diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index 92c2910..538a706 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -10,6 +10,7 @@ import {MockPriceFeed} from "./MockPriceFeed.sol"; contract BigSignalTest is Test { BitSignal public bitsignal; MockPriceFeed public mockUsdcPriceFeed; + MockPriceFeed public mockBtcPriceFeed; IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // 6 decimals IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); // 8 decimals IERC20 constant USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); @@ -22,9 +23,10 @@ contract BigSignalTest is Test { address USDC_PRICE_FEED_ADDRESS = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; function setUp() public { - mockUsdcPriceFeed = new MockPriceFeed(); + mockUsdcPriceFeed = new MockPriceFeed(8); + mockBtcPriceFeed = new MockPriceFeed(8); vm.prank(arbitor); - bitsignal = new BitSignal(balajis, counterparty, address(mockUsdcPriceFeed)); + bitsignal = new BitSignal(balajis, counterparty, address(mockUsdcPriceFeed), address(mockBtcPriceFeed)); } function testSettle() public { @@ -33,32 +35,36 @@ contract BigSignalTest is Test { console2.log(price); } - function testDepositAndInitiateBetAndSettle() public { - console2.logString('testDepositAndInitiateBetAndSettle beginning...'); - console2.logString('USDC whale balance:'); - uint256 usdsWhaleBalance = USDC.balanceOf(usdcWhale); - console2.logUint(usdsWhaleBalance); - console2.log('USDC whale balance: %d', usdsWhaleBalance); - // Fund balajis and counterparty for test prep - vm.prank(usdcWhale); - USDC.transfer(balajis, 1_000_000e6); - console2.logString('USDC been transferred'); - vm.prank(wbtcWhale); - WBTC.transfer(counterparty, 1e8); - console.logString('WBTC been transferred'); - + function fundParticipantWallets() private { + console2.logString('USDC whale balance:'); + uint256 usdsWhaleBalance = USDC.balanceOf(usdcWhale); + console2.logUint(usdsWhaleBalance); + console2.log('USDC whale balance: %d', usdsWhaleBalance); + // Fund balajis and counterparty for test prep + vm.prank(usdcWhale); + USDC.transfer(balajis, 1_000_000e6); + console2.logString('USDC been transferred'); + vm.prank(wbtcWhale); + WBTC.transfer(counterparty, 1e8); + console.logString('WBTC been transferred'); + } - // Now simulate the deposits - vm.startPrank(balajis); - USDC.approve(address(bitsignal), type(uint256).max); - bitsignal.depositUSDC(); - vm.stopPrank(); + function startBet() private { + // Now simulate the deposits + vm.startPrank(balajis); + USDC.approve(address(bitsignal), type(uint256).max); + bitsignal.depositUSDC(); + vm.stopPrank(); - vm.startPrank(counterparty); - WBTC.approve(address(bitsignal), type(uint256).max); - bitsignal.depositWBTC(); - vm.stopPrank(); + vm.startPrank(counterparty); + WBTC.approve(address(bitsignal), type(uint256).max); + bitsignal.depositWBTC(); + vm.stopPrank(); + } + function testDepositAndInitiateBetAndSettleWhenCounterpartyWins() public { + fundParticipantWallets(); + startBet(); // Prevent settlement before the bet has expired vm.startPrank(counterparty); vm.expectRevert("bet not finished"); @@ -69,6 +75,7 @@ contract BigSignalTest is Test { // Successfully settle after expiry vm.warp(block.timestamp + 100 days); + mockBtcPriceFeed.setAnswer(999_999 * 1e8); bitsignal.settle(); // Check that winnings received @@ -76,12 +83,26 @@ contract BigSignalTest is Test { assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); } + function testDepositAndInitiateBetAndSettleWhenBalajisWins() public { + fundParticipantWallets(); + startBet(); + + uint256 usdcBeforeSettlement = USDC.balanceOf(balajis); + uint256 wbtcBeforeSettlement = WBTC.balanceOf(balajis); + + // Successfully settle after expiry + vm.warp(block.timestamp + 100 days); + mockBtcPriceFeed.setAnswer(1_000_001 * 1e8); + bitsignal.settle(); + + // Check that winnings received + assertEq(USDC.balanceOf(balajis), usdcBeforeSettlement + 1_000_000e6); + assertEq(WBTC.balanceOf(balajis), wbtcBeforeSettlement + 1e8); + } + + function testDepositAndCancelBeforeInitiating() public { - // Fund balajis and counterparty for test prep - vm.prank(usdcWhale); - USDC.transfer(balajis, 1_000_000e6); - vm.prank(wbtcWhale); - WBTC.transfer(counterparty, 1e8); + fundParticipantWallets(); // Now simulate the deposits vm.startPrank(balajis); @@ -96,26 +117,8 @@ contract BigSignalTest is Test { } function testSwap() public { - // Fund balajis and counterparty for test prep - vm.prank(usdcWhale); - USDC.transfer(balajis, 1_000_000e6); - vm.prank(wbtcWhale); - WBTC.transfer(counterparty, 1e8); - - - vm.startPrank(balajis); - USDC.approve(address(bitsignal), type(uint256).max); - bitsignal.depositUSDC(); - vm.stopPrank(); - - vm.prank(arbitor); - vm.expectRevert("bet is not initiated"); - bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); - - vm.startPrank(counterparty); - WBTC.approve(address(bitsignal), type(uint256).max); - bitsignal.depositWBTC(); - vm.stopPrank(); + fundParticipantWallets(); + startBet(); mockUsdcPriceFeed.setAnswer(99000000); @@ -141,10 +144,7 @@ contract BigSignalTest is Test { console2.log("USDT balance after swap: %d", USDT.balanceOf(address(bitsignal))); console2.log("balajis address: %s", balajis); console2.log("counterparty address: %s", counterparty); - - console2.log(bitsignal.betInitiated()); - uint256 usdtBeforeSettlement = USDT.balanceOf(counterparty); uint256 wbtcBeforeSettlement = WBTC.balanceOf(counterparty); @@ -157,6 +157,35 @@ contract BigSignalTest is Test { assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); } + function testOnRealPriceFeeds() public { + bitsignal = new BitSignal( + balajis, + counterparty, + address(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6), + address(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c) + ); + + AggregatorV3Interface btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals + AggregatorV3Interface usdcPriceFeed = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); // 8 decimals + uint256 btcPrice = bitsignal.chainlinkPrice(btcPriceFeed) / 1e8; + uint256 usdcPrice = bitsignal.chainlinkPrice(usdcPriceFeed); + console2.log("BTC price: %d", btcPrice); + console2.log("USDC price: %d", usdcPrice); + fundParticipantWallets(); + startBet(); + + // Successfully settle after expiry + vm.warp(block.timestamp + 100 days); + bitsignal.settle(); + + console2.log("WBTC balance of balajis: %d", WBTC.balanceOf(balajis) / 10**WBTC.decimals()); + console2.log("USDC balance of balajis: %d", USDC.balanceOf(balajis) / 10**USDC.decimals()); + console2.log("WBTC balance of counterparty: %d", WBTC.balanceOf(counterparty) / 10**WBTC.decimals()); + console2.log("USDC balance of counterparty: %d", USDC.balanceOf(counterparty) / 10**USDC.decimals()); + } + + + } diff --git a/test/MockPriceFeed.sol b/test/MockPriceFeed.sol index 0d1b30c..ba76267 100644 --- a/test/MockPriceFeed.sol +++ b/test/MockPriceFeed.sol @@ -6,13 +6,18 @@ import {AggregatorV3Interface} from '../src/BitSignal.sol'; contract MockPriceFeed is AggregatorV3Interface { int256 internal price; + uint8 decimalsNumber; + + constructor(uint8 _decimals) { + decimalsNumber = _decimals; + } function setAnswer(int256 _answer) external { price = _answer; } function decimals() external view returns (uint8) { - return 8; + return decimalsNumber; } function latestRoundData() From d90f8d683765608e6634dd8f1732b5a0663b610b Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sun, 9 Apr 2023 10:06:58 -0500 Subject: [PATCH 10/16] Add test for change transfer --- src/BitSignal.sol | 8 ------- test/BitSignal.t.sol | 52 +++++++++++++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index baa4347..45f56c4 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -6,8 +6,6 @@ import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; import '@openzeppelin/contracts/access/Ownable.sol'; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "forge-std/console2.sol"; - interface AggregatorV3Interface { function decimals() external view returns (uint8); @@ -158,17 +156,11 @@ contract BitSignal is Ownable { winner = counterparty; } - console2.log('Winner choosen %s', winner); - console2.log('Collateral address: %s', address(collateral)); - console2.log('Collateral balance: %d', collateral.balanceOf(address(this))); - SafeERC20.safeTransfer(collateral, winner, collateral.balanceOf(address(this))); //collateral.transfer(winner, collateral.balanceOf(address(this))); - console2.log('Collateral transferred'); WBTC.transfer(winner, WBTC.balanceOf(address(this))); // in case there wasn't enough liquidity in Uniswap pool and some USDC change left if (address(collateral) != address(USDC)) { - console2.log('Before change transfer'); USDC.transfer(winner, USDC.balanceOf(address(this))); } } diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index 538a706..0b21b56 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -29,17 +29,17 @@ contract BigSignalTest is Test { bitsignal = new BitSignal(balajis, counterparty, address(mockUsdcPriceFeed), address(mockBtcPriceFeed)); } - function testSettle() public { + function testSettle() view public { AggregatorV3Interface btcPriceFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); // 8 decimals uint256 price = bitsignal.chainlinkPrice(btcPriceFeed) / 1e8; - console2.log(price); + console2.log("BTC price: %s", price); } function fundParticipantWallets() private { - console2.logString('USDC whale balance:'); - uint256 usdsWhaleBalance = USDC.balanceOf(usdcWhale); - console2.logUint(usdsWhaleBalance); - console2.log('USDC whale balance: %d', usdsWhaleBalance); + uint256 usdcWhaleBalance = USDC.balanceOf(usdcWhale); + console2.log('USDC whale balance: %d', usdcWhaleBalance); + uint256 wbtcWhaleBalance = WBTC.balanceOf(wbtcWhale); + console2.log('WBTC whale balance: %d', wbtcWhaleBalance); // Fund balajis and counterparty for test prep vm.prank(usdcWhale); USDC.transfer(balajis, 1_000_000e6); @@ -150,6 +150,7 @@ contract BigSignalTest is Test { // Successfully settle after expiry vm.warp(block.timestamp + 100 days); + mockBtcPriceFeed.setAnswer(999_999 * 1e8); bitsignal.settle(); // Check that winnings received @@ -157,6 +158,34 @@ contract BigSignalTest is Test { assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); } + function testChangeBeenTransferred() public { + fundParticipantWallets(); + startBet(); + mockUsdcPriceFeed.setAnswer(95000000); + vm.prank(arbitor); + uint256 swapOutput = bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + // simulate change after swap + uint256 change = 10_000*10**USDC.decimals(); + vm.prank(usdcWhale); + USDC.transfer(address(bitsignal), change); + + + uint256 usdtBeforeSettlement = USDT.balanceOf(counterparty); + uint256 usdcBeforeSettlement = USDC.balanceOf(counterparty); + uint256 wbtcBeforeSettlement = WBTC.balanceOf(counterparty); + + // Successfully settle after expiry + vm.warp(block.timestamp + 100 days); + mockBtcPriceFeed.setAnswer(999_999 * 1e8); + bitsignal.settle(); + + // Check that winnings received + assertEq(USDT.balanceOf(counterparty), usdtBeforeSettlement + swapOutput); + assertEq(USDC.balanceOf(counterparty), usdcBeforeSettlement + change); + assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); + + } + function testOnRealPriceFeeds() public { bitsignal = new BitSignal( balajis, @@ -187,14 +216,3 @@ contract BigSignalTest is Test { } - - -// test balajis wins the bet -// test counterparty wins the bet -// test swap is not allowed to anyone other than arbitor -// test swap is not allowed before bet is initiated -// test swap is not allowed if USDC haven't lost is peg -// test winner recieves alternatiwe stablecoin after swap -// test winner receives a change if not all amount of USDC been swapped -// test on fork and on mainner for price feed - From 4e512b01e6b22de025cbdd4ae380262c17a79a00 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sun, 9 Apr 2023 10:09:32 -0500 Subject: [PATCH 11/16] Add DAI to stable-coins list --- src/BitSignal.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index 45f56c4..bae7368 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -51,7 +51,8 @@ contract BitSignal is Ownable { address(USDC), // Circle USDC 0xdAC17F958D2ee523a2206206994597C13D831ec7, // Tether USDT 0x4Fabb145d64652a948d72533023f6E7A623C7C53, // Binance BUSD - 0x8E870D67F660D95d5be530380D0eC0bd388289E1 // Paxos USDP + 0x8E870D67F660D95d5be530380D0eC0bd388289E1, // Paxos USDP + 0x6B175474E89094C44Da98b954EedeAC495271d0F // DAI stablecoin ]; modifier swapAllowed(address token) { @@ -62,7 +63,7 @@ contract BitSignal is Ownable { uint256 usdcPrice = chainlinkPrice(usdcPriceFeed); require(usdcPrice <= STABLECOIN_MIN_PRICE, "Collateral coin haven`t lost its peg"); bool found; - for (uint i=0; i<4; i++) { + for (uint i=0; i<5; i++) { if (STABLECOIN_CONTRACTS[i] == token) { found = true; } From 6fe83b16455b4c61e7478f52fdcbe64bd81dab5d Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sun, 9 Apr 2023 11:07:39 -0500 Subject: [PATCH 12/16] Add params for multihop swap --- script/Deploy.s.sol | 4 ++-- src/BitSignal.sol | 26 +++++++++++++++----------- test/BitSignal.t.sol | 23 ++++++++++++++++++----- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index e081acf..a058161 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -12,8 +12,8 @@ contract BitSignalScript is Script { vm.startBroadcast(); BitSignal bitsignal = new BitSignal( - address(0x1), - address(0x2), + address(0x53Cfaa403a214c9be35011B3Dcfb75D81D2F7B6B), + address(0x83d47D101881A1E52Ae9C6A2272f499601b8fBCF), 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6, 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c ); diff --git a/src/BitSignal.sol b/src/BitSignal.sol index bae7368..bf6b91f 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -55,13 +55,15 @@ contract BitSignal is Ownable { 0x6B175474E89094C44Da98b954EedeAC495271d0F // DAI stablecoin ]; - modifier swapAllowed(address token) { + modifier swapAllowed(address[] calldata hops) { // this function will be used only in case of emergency // that`s why its better to consume more gas here due to search in array // rather than building map in constructor require(betInitiated, "bet is not initiated"); uint256 usdcPrice = chainlinkPrice(usdcPriceFeed); require(usdcPrice <= STABLECOIN_MIN_PRICE, "Collateral coin haven`t lost its peg"); + require(hops.length > 1, "Should be at leas one hoop"); + address token = hops[hops.length-1]; bool found; for (uint i=0; i<5; i++) { if (STABLECOIN_CONTRACTS[i] == token) { @@ -79,26 +81,29 @@ contract BitSignal is Ownable { btcPriceFeed = AggregatorV3Interface(_btcPriceFeedAddress); // 8 decimals } + function _encodePathV3(address[] calldata _hops, uint24[] calldata _fees) internal view returns (bytes memory path) { + require(_fees.length == _hops.length, "Wrong fees count"); + path = abi.encodePacked(address(USDC)); + for(uint i = 0; i < _hops.length; i++){ + path = abi.encodePacked(path, _fees[i], _hops[i]); + } + return path; + } + /// @notice Let arbitor to swap collateral in case deposited stablecoin starts to loose it's peg - function swapCollateral(address token, uint256 amountMinimum, uint24 feeToWeth, uint24 feeFromWeth) external onlyOwner swapAllowed(token) returns (uint256) { + function swapCollateral(uint256 amountMinimum, address[] calldata hops, uint24[] calldata fees) external onlyOwner swapAllowed(hops) returns (uint256) { ISwapRouter swapRouter = ISwapRouter(UNISWAP_ROUTER); TransferHelper.safeApprove(address(USDC), UNISWAP_ROUTER, USDC_AMOUNT); ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked( - address(USDC), - feeToWeth, - WETH_CONTRACT, - feeFromWeth, - token - ), + path: _encodePathV3(hops, fees), recipient: address(this), deadline: block.timestamp, amountIn: USDC_AMOUNT, amountOutMinimum: amountMinimum }); uint256 output = swapRouter.exactInput(params); - collateral = IERC20(token); + collateral = IERC20(hops[hops.length-1]); return output; } @@ -158,7 +163,6 @@ contract BitSignal is Ownable { } SafeERC20.safeTransfer(collateral, winner, collateral.balanceOf(address(this))); - //collateral.transfer(winner, collateral.balanceOf(address(this))); WBTC.transfer(winner, WBTC.balanceOf(address(this))); // in case there wasn't enough liquidity in Uniswap pool and some USDC change left if (address(collateral) != address(USDC)) { diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index 0b21b56..5722b93 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -22,10 +22,18 @@ contract BigSignalTest is Test { address wbtcWhale = 0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656; address USDC_PRICE_FEED_ADDRESS = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; + uint24[] fees; + address[] hops; + + function setUp() public { mockUsdcPriceFeed = new MockPriceFeed(8); mockBtcPriceFeed = new MockPriceFeed(8); vm.prank(arbitor); + fees.push(uint24(500)); + fees.push(uint24(3000)); + hops.push(address(WETH9)); + hops.push(address(USDT)); bitsignal = new BitSignal(balajis, counterparty, address(mockUsdcPriceFeed), address(mockBtcPriceFeed)); } @@ -124,20 +132,25 @@ contract BigSignalTest is Test { vm.prank(arbitor); vm.expectRevert("Collateral coin haven`t lost its peg"); - bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + + bitsignal.swapCollateral(900_000e6, hops, fees); mockUsdcPriceFeed.setAnswer(95000000); vm.prank(balajis); vm.expectRevert("Ownable: caller is not the owner"); - bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + bitsignal.swapCollateral(900_000e6, hops, fees); vm.prank(arbitor); vm.expectRevert("swap to choosen token is not allowed"); - bitsignal.swapCollateral(address(WETH9), 900_000e6, 500, 3000); + hops.push(address(WBTC)); + fees.push(3000); + bitsignal.swapCollateral(900_000e6, hops, fees); + hops.pop(); + fees.pop(); vm.prank(arbitor); - uint256 swapOutput = bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + uint256 swapOutput = bitsignal.swapCollateral(900_000e6, hops, fees); console2.log("SwapCollateral output: %d", swapOutput); console2.log("USDC balance after swap: %d", USDC.balanceOf(address(bitsignal))); @@ -163,7 +176,7 @@ contract BigSignalTest is Test { startBet(); mockUsdcPriceFeed.setAnswer(95000000); vm.prank(arbitor); - uint256 swapOutput = bitsignal.swapCollateral(address(USDT), 900_000e6, 500, 3000); + uint256 swapOutput = bitsignal.swapCollateral(900_000e6, hops, fees); // simulate change after swap uint256 change = 10_000*10**USDC.decimals(); vm.prank(usdcWhale); From 9638f4a28d1553971c9902e5a470fc71fde04ddf Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Sun, 9 Apr 2023 12:05:06 -0500 Subject: [PATCH 13/16] Claim reward after settle --- src/BitSignal.sol | 21 ++++++++--------- test/BitSignal.t.sol | 54 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index bf6b91f..197947d 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -37,9 +37,9 @@ contract BitSignal is Ownable { AggregatorV3Interface immutable btcPriceFeed; AggregatorV3Interface immutable usdcPriceFeed; - IERC20 collateral = USDC; address public immutable balajis; address public immutable counterparty; + address public winner; bool internal usdcDeposited; bool internal wbtcDeposited; @@ -99,11 +99,10 @@ contract BitSignal is Ownable { path: _encodePathV3(hops, fees), recipient: address(this), deadline: block.timestamp, - amountIn: USDC_AMOUNT, + amountIn: USDC.balanceOf(address(this)), amountOutMinimum: amountMinimum }); uint256 output = swapRouter.exactInput(params); - collateral = IERC20(hops[hops.length-1]); return output; } @@ -146,6 +145,14 @@ contract BitSignal is Ownable { } } + /// @notice This will let the winner to take out any token from contract + function claim(address token) external { + require(msg.sender == winner, "Imposter!"); + // any contract cound be passed as an input - looks as a security breach + // but there is no need for safety check - we already checked that sender is a winner and he should be allowed to do whatewer + SafeERC20.safeTransfer(IERC20(token), winner, IERC20(token).balanceOf(address(this))); + } + /// @notice Once 90 days have passed, query Chainlink BTC/USD price feed to determine the winner and send them both collaterals. function settle() external { require(betInitiated, "bet not initiated"); @@ -155,19 +162,11 @@ contract BitSignal is Ownable { uint256 wbtcPrice = chainlinkPrice(btcPriceFeed) / 10**btcPriceFeed.decimals(); - address winner; if (wbtcPrice >= PRICE_THRESHOLD) { winner = balajis; } else { winner = counterparty; } - - SafeERC20.safeTransfer(collateral, winner, collateral.balanceOf(address(this))); - WBTC.transfer(winner, WBTC.balanceOf(address(this))); - // in case there wasn't enough liquidity in Uniswap pool and some USDC change left - if (address(collateral) != address(USDC)) { - USDC.transfer(winner, USDC.balanceOf(address(this))); - } } /// @notice Fetch the token price with 8 decimals included diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index 5722b93..a8ef0d0 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -86,6 +86,20 @@ contract BigSignalTest is Test { mockBtcPriceFeed.setAnswer(999_999 * 1e8); bitsignal.settle(); + // check that lost party cannot claim reward + vm.stopPrank(); + vm.startPrank(balajis); + vm.expectRevert("Imposter!"); + bitsignal.claim(address(USDC)); + vm.expectRevert("Imposter!"); + bitsignal.claim(address(WBTC)); + + // claim reward + vm.stopPrank(); + vm.startPrank(counterparty); + bitsignal.claim(address(USDC)); + bitsignal.claim(address(WBTC)); + // Check that winnings received assertEq(USDC.balanceOf(counterparty), usdcBeforeSettlement + 1_000_000e6); assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); @@ -103,6 +117,19 @@ contract BigSignalTest is Test { mockBtcPriceFeed.setAnswer(1_000_001 * 1e8); bitsignal.settle(); + vm.startPrank(counterparty); + vm.expectRevert("Imposter!"); + bitsignal.claim(address(USDC)); + vm.expectRevert("Imposter!"); + bitsignal.claim(address(WBTC)); + + //claim reward + vm.stopPrank(); + vm.startPrank(balajis); + bitsignal.claim(address(USDC)); + bitsignal.claim(address(WBTC)); + + // Check that winnings received assertEq(USDC.balanceOf(balajis), usdcBeforeSettlement + 1_000_000e6); assertEq(WBTC.balanceOf(balajis), wbtcBeforeSettlement + 1e8); @@ -166,18 +193,23 @@ contract BigSignalTest is Test { mockBtcPriceFeed.setAnswer(999_999 * 1e8); bitsignal.settle(); + //claim reward + vm.startPrank(counterparty); + bitsignal.claim(address(USDT)); + bitsignal.claim(address(WBTC)); + // Check that winnings received assertEq(USDT.balanceOf(counterparty), usdtBeforeSettlement + swapOutput); assertEq(WBTC.balanceOf(counterparty), wbtcBeforeSettlement + 1e8); } - function testChangeBeenTransferred() public { + function testClaimChange() public { fundParticipantWallets(); startBet(); mockUsdcPriceFeed.setAnswer(95000000); vm.prank(arbitor); uint256 swapOutput = bitsignal.swapCollateral(900_000e6, hops, fees); - // simulate change after swap + // simulate non-complete swap uint256 change = 10_000*10**USDC.decimals(); vm.prank(usdcWhale); USDC.transfer(address(bitsignal), change); @@ -192,6 +224,13 @@ contract BigSignalTest is Test { mockBtcPriceFeed.setAnswer(999_999 * 1e8); bitsignal.settle(); + //claim reward + vm.startPrank(counterparty); + bitsignal.claim(address(USDC)); + bitsignal.claim(address(USDT)); + bitsignal.claim(address(WBTC)); + + // Check that winnings received assertEq(USDT.balanceOf(counterparty), usdtBeforeSettlement + swapOutput); assertEq(USDC.balanceOf(counterparty), usdcBeforeSettlement + change); @@ -220,6 +259,17 @@ contract BigSignalTest is Test { vm.warp(block.timestamp + 100 days); bitsignal.settle(); + if (bitsignal.winner() == balajis) { + vm.startPrank(balajis); + } else { + vm.startPrank(counterparty); + } + + //claim reward + bitsignal.claim(address(USDC)); + bitsignal.claim(address(USDT)); + bitsignal.claim(address(WBTC)); + console2.log("WBTC balance of balajis: %d", WBTC.balanceOf(balajis) / 10**WBTC.decimals()); console2.log("USDC balance of balajis: %d", USDC.balanceOf(balajis) / 10**USDC.decimals()); console2.log("WBTC balance of counterparty: %d", WBTC.balanceOf(counterparty) / 10**WBTC.decimals()); From 151b5768e0f4661fab55b5e8a1c21dd8936b41f5 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 9 Apr 2023 14:11:40 -0500 Subject: [PATCH 14/16] emit events on start and settle --- src/BitSignal.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index 197947d..22dc621 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -23,6 +23,9 @@ interface AggregatorV3Interface { contract BitSignal is Ownable { + event BetStarted(); + event BetSettled(); + uint256 constant BET_LENGTH = 90 days; uint256 constant PRICE_THRESHOLD = 1_000_000; // 1 million USD per BTC uint256 constant USDC_AMOUNT = 1_000_000e6; @@ -115,6 +118,7 @@ contract BitSignal is Ownable { if (wbtcDeposited) { betInitiated = true; startTimestamp = block.timestamp; + emit BetStarted(); } } @@ -126,6 +130,7 @@ contract BitSignal is Ownable { if (usdcDeposited) { betInitiated = true; + emit BetStarted(); startTimestamp = block.timestamp; } } @@ -167,6 +172,8 @@ contract BitSignal is Ownable { } else { winner = counterparty; } + + emit BetSettled(); } /// @notice Fetch the token price with 8 decimals included From 80f742785c03a2dc16b933867fc4c0843f555b74 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 9 Apr 2023 14:13:48 -0500 Subject: [PATCH 15/16] Add winner to BetSettle event --- src/BitSignal.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BitSignal.sol b/src/BitSignal.sol index 22dc621..45eb9fd 100644 --- a/src/BitSignal.sol +++ b/src/BitSignal.sol @@ -24,7 +24,7 @@ interface AggregatorV3Interface { contract BitSignal is Ownable { event BetStarted(); - event BetSettled(); + event BetSettled(address winner); uint256 constant BET_LENGTH = 90 days; uint256 constant PRICE_THRESHOLD = 1_000_000; // 1 million USD per BTC @@ -173,7 +173,7 @@ contract BitSignal is Ownable { winner = counterparty; } - emit BetSettled(); + emit BetSettled(winner); } /// @notice Fetch the token price with 8 decimals included From 4aa396b492b426cc7443c71aa3ba328cd5b63193 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 9 Apr 2023 19:45:34 -0500 Subject: [PATCH 16/16] add one more test case --- test/BitSignal.t.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/BitSignal.t.sol b/test/BitSignal.t.sol index a8ef0d0..4980aa0 100644 --- a/test/BitSignal.t.sol +++ b/test/BitSignal.t.sol @@ -153,8 +153,11 @@ contract BigSignalTest is Test { function testSwap() public { fundParticipantWallets(); - startBet(); + vm.prank(arbitor); + vm.expectRevert("bet is not initiated"); + bitsignal.swapCollateral(900_000e6, hops, fees); + startBet(); mockUsdcPriceFeed.setAnswer(99000000); vm.prank(arbitor);