From 7c3ece2e40a76b006a7a160fb9e3a6c4033a8511 Mon Sep 17 00:00:00 2001 From: StakeDAOHQ Date: Mon, 28 Feb 2022 18:38:14 +0530 Subject: [PATCH] feat: add all contracts --- angle | 1 + avax | 1 + convex | 1 + harmony | 1 + nft/abis/BaseRewardPool.json | 363 +++++++ nft/abis/Controller.json | 320 ++++++ nft/abis/ERC20.json | 159 +++ nft/abis/Palace.json | 1 + nft/abis/StakeDaoNFTPalace.json | 1 + nft/abis/YVault.json | 347 ++++++ nft/contracts/DarkParadise.sol | 655 ++++++++++++ nft/contracts/ERC1155Tradable.sol | 1241 +++++++++++++++++++++ nft/contracts/StakeDaoNFTPalace.sol | 119 +++ nft/contracts/StakeDaoNFT_V2.sol | 202 ++++ nft/hardhat.config.ts | 47 + nft/package.json | 35 + nft/readme.md | 3 + nft/test/contracturi.json | 6 + nft/test/strategyArb.ts | 1363 ++++++++++++++++++++++++ nft/tsconfig.json | 12 + polygon/abis/BaseRewardPool.json | 363 +++++++ polygon/abis/Controller.json | 320 ++++++ polygon/abis/ERC20.json | 159 +++ polygon/abis/YVault.json | 347 ++++++ polygon/contracts/StrategyAm3Crv.sol | 252 +++++ polygon/contracts/StrategyBtcCurve.sol | 260 +++++ polygon/contracts/Vault.sol | 144 +++ polygon/hardhat.config.ts | 46 + polygon/package.json | 35 + polygon/readme.md | 3 + polygon/test/strategyAm3crv.ts | 85 ++ polygon/test/strategySbtc.ts | 243 +++++ polygon/tsconfig.json | 12 + protocol | 1 + 34 files changed, 7148 insertions(+) create mode 160000 angle create mode 160000 avax create mode 160000 convex create mode 160000 harmony create mode 100644 nft/abis/BaseRewardPool.json create mode 100644 nft/abis/Controller.json create mode 100644 nft/abis/ERC20.json create mode 100644 nft/abis/Palace.json create mode 100644 nft/abis/StakeDaoNFTPalace.json create mode 100644 nft/abis/YVault.json create mode 100644 nft/contracts/DarkParadise.sol create mode 100644 nft/contracts/ERC1155Tradable.sol create mode 100644 nft/contracts/StakeDaoNFTPalace.sol create mode 100644 nft/contracts/StakeDaoNFT_V2.sol create mode 100644 nft/hardhat.config.ts create mode 100644 nft/package.json create mode 100644 nft/readme.md create mode 100644 nft/test/contracturi.json create mode 100644 nft/test/strategyArb.ts create mode 100644 nft/tsconfig.json create mode 100644 polygon/abis/BaseRewardPool.json create mode 100644 polygon/abis/Controller.json create mode 100644 polygon/abis/ERC20.json create mode 100644 polygon/abis/YVault.json create mode 100644 polygon/contracts/StrategyAm3Crv.sol create mode 100644 polygon/contracts/StrategyBtcCurve.sol create mode 100644 polygon/contracts/Vault.sol create mode 100644 polygon/hardhat.config.ts create mode 100644 polygon/package.json create mode 100644 polygon/readme.md create mode 100644 polygon/test/strategyAm3crv.ts create mode 100644 polygon/test/strategySbtc.ts create mode 100644 polygon/tsconfig.json create mode 160000 protocol diff --git a/angle b/angle new file mode 160000 index 0000000..4649632 --- /dev/null +++ b/angle @@ -0,0 +1 @@ +Subproject commit 46496329f667c0ee1786fc14f0fe79ca3cb12b7b diff --git a/avax b/avax new file mode 160000 index 0000000..e15b9f4 --- /dev/null +++ b/avax @@ -0,0 +1 @@ +Subproject commit e15b9f4c1f155d7150eeed31f9f3e3ad3f158bc7 diff --git a/convex b/convex new file mode 160000 index 0000000..6ba93b2 --- /dev/null +++ b/convex @@ -0,0 +1 @@ +Subproject commit 6ba93b253bd23ef7986858b658ae8b1009db3b36 diff --git a/harmony b/harmony new file mode 160000 index 0000000..4bf1656 --- /dev/null +++ b/harmony @@ -0,0 +1 @@ +Subproject commit 4bf1656086c4c79253f3f78f7eef272ba548b461 diff --git a/nft/abis/BaseRewardPool.json b/nft/abis/BaseRewardPool.json new file mode 100644 index 0000000..ffc42dc --- /dev/null +++ b/nft/abis/BaseRewardPool.json @@ -0,0 +1,363 @@ +[ + { + "inputs": [ + {"internalType": "uint256", "name": "pid_", "type": "uint256"}, + {"internalType": "address", "name": "stakingToken_", "type": "address"}, + {"internalType": "address", "name": "rewardToken_", "type": "address"}, + {"internalType": "address", "name": "operator_", "type": "address"}, + {"internalType": "address", "name": "rewardManager_", "type": "address"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [ + {"internalType": "address", "name": "_reward", "type": "address"} + ], + "name": "addExtraReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "clearExtraRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "donate", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "duration", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "earned", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "name": "extraRewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "extraRewardsLength", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_account", "type": "address"}, + {"internalType": "bool", "name": "_claimExtras", "type": "bool"} + ], + "name": "getReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "historicalRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastUpdateTime", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "newRewardRatio", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "operator", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "periodFinish", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pid", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_rewards", "type": "uint256"} + ], + "name": "queueNewRewards", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "queuedRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardManager", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "rewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "stake", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakeAll", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_for", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "stakeFor", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakingToken", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "userRewardPerTokenPaid", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "bool", "name": "claim", "type": "bool"} + ], + "name": "withdraw", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool", "name": "claim", "type": "bool"}], + "name": "withdrawAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool", "name": "claim", "type": "bool"}], + "name": "withdrawAllAndUnwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "bool", "name": "claim", "type": "bool"} + ], + "name": "withdrawAndUnwrap", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/nft/abis/Controller.json b/nft/abis/Controller.json new file mode 100644 index 0000000..5241e1d --- /dev/null +++ b/nft/abis/Controller.json @@ -0,0 +1,320 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "approveStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "approvedStrategies", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "converters", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "getExpectedReturn", + "outputs": [ + {"internalType": "uint256", "name": "expected", "type": "uint256"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "inCaseStrategyTokenGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "inCaseTokensGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onesplit", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "revokeStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_input", "type": "address"}, + {"internalType": "address", "name": "_output", "type": "address"}, + {"internalType": "address", "name": "_converter", "type": "address"} + ], + "name": "setConverter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_onesplit", "type": "address"} + ], + "name": "setOneSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "name": "setRewards", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_split", "type": "uint256"} + ], + "name": "setSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategist", "type": "address"} + ], + "name": "setStrategist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "setStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_vault", "type": "address"} + ], + "name": "setVault", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "split", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "strategies", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "strategist", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "vaults", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "yearn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/nft/abis/ERC20.json b/nft/abis/ERC20.json new file mode 100644 index 0000000..f43b285 --- /dev/null +++ b/nft/abis/ERC20.json @@ -0,0 +1,159 @@ +[ + { + "name": "Transfer", + "inputs": [ + {"type": "address", "name": "_from", "indexed": true}, + {"type": "address", "name": "_to", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Approval", + "inputs": [ + {"type": "address", "name": "_owner", "indexed": true}, + {"type": "address", "name": "_spender", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"}, + {"type": "uint256", "name": "_decimals"}, + {"type": "uint256", "name": "_supply"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "set_minter", + "outputs": [], + "inputs": [{"type": "address", "name": "_minter"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 36247 + }, + { + "name": "set_name", + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 178069 + }, + { + "name": "totalSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1211 + }, + { + "name": "allowance", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "address", "name": "_owner"}, + {"type": "address", "name": "_spender"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1549 + }, + { + "name": "transfer", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 74832 + }, + { + "name": "transferFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 111983 + }, + { + "name": "approve", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_spender"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 39078 + }, + { + "name": "mint", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75808 + }, + { + "name": "burnFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75826 + }, + { + "name": "name", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 7823 + }, + { + "name": "symbol", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 6876 + }, + { + "name": "decimals", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1481 + }, + { + "name": "balanceOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1665 + } +] diff --git a/nft/abis/Palace.json b/nft/abis/Palace.json new file mode 100644 index 0000000..18b62bb --- /dev/null +++ b/nft/abis/Palace.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"contract ERC1155Tradable","name":"_nftAddress","type":"address"},{"internalType":"contract IERC20","name":"_xsdt","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"card","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"points","type":"uint256"}],"name":"CardAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract ERC1155Tradable","name":"newNFT","type":"address"}],"name":"NFTSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Redeemed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cardId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"addCard","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"cards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"earned","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"lastUpdateTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"nft","outputs":[{"internalType":"contract ERC1155Tradable","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"points","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"card","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC1155Tradable","name":"_nftAddress","type":"address"}],"name":"setNFT","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"stake","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"xsdt","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/nft/abis/StakeDaoNFTPalace.json b/nft/abis/StakeDaoNFTPalace.json new file mode 100644 index 0000000..18b62bb --- /dev/null +++ b/nft/abis/StakeDaoNFTPalace.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"contract ERC1155Tradable","name":"_nftAddress","type":"address"},{"internalType":"contract IERC20","name":"_xsdt","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"card","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"points","type":"uint256"}],"name":"CardAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract ERC1155Tradable","name":"newNFT","type":"address"}],"name":"NFTSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Redeemed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cardId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"addCard","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"cards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"earned","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"lastUpdateTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"nft","outputs":[{"internalType":"contract ERC1155Tradable","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"points","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"card","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC1155Tradable","name":"_nftAddress","type":"address"}],"name":"setNFT","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"stake","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"xsdt","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/nft/abis/YVault.json b/nft/abis/YVault.json new file mode 100644 index 0000000..6ba3c86 --- /dev/null +++ b/nft/abis/YVault.json @@ -0,0 +1,347 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "available", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "balance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "controller", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"} + ], + "name": "decreaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "deposit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "depositAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPricePerFullShare", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "reserve", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "harvest", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "addedValue", "type": "uint256"} + ], + "name": "increaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "min", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "name": "setController", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{"internalType": "uint256", "name": "_min", "type": "uint256"}], + "name": "setMin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_shares", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/nft/contracts/DarkParadise.sol b/nft/contracts/DarkParadise.sol new file mode 100644 index 0000000..bc780ef --- /dev/null +++ b/nft/contracts/DarkParadise.sol @@ -0,0 +1,655 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.5.0; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow, so we distribute + return (a / 2) + (b / 2) + (((a % 2) + (b % 2)) / 2); + } +} + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + require(b <= a, "SafeMath: subtraction overflow"); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, "SafeMath: division by zero"); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0, "SafeMath: modulo by zero"); + return a % b; + } +} + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the `nonReentrant` modifier + * available, which can be aplied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + */ +contract ReentrancyGuard { + /// @dev counter to allow mutex lock with only one SSTORE operation + uint256 private _guardCounter; + + constructor() internal { + // The counter starts at one to prevent changing it from zero to a non-zero + // value, which is a more expensive operation. + _guardCounter = 1; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _guardCounter += 1; + uint256 localCounter = _guardCounter; + _; + require( + localCounter == _guardCounter, + "ReentrancyGuard: reentrant call" + ); + } +} + +interface IERC20 { + // ERC20 Optional Views + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + // Views + function totalSupply() external view returns (uint256); + + function balanceOf(address owner) external view returns (uint256); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + // Mutative functions + function transfer(address to, uint256 value) external returns (bool); + + function approve(address spender, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + // Events + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +/** + * @dev Collection of functions related to the address type, + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * This test is non-exhaustive, and there may be false-negatives: during the + * execution of a contract's constructor, its address will be reported as + * not containing a contract. + * + * > It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + */ + function isContract(address account) internal view returns (bool) { + // This method relies in extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { + size := extcodesize(account) + } + return size > 0; + } +} + +library SafeERC20 { + using SafeMath for uint256; + using Address for address; + + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + callOptionalReturn( + token, + abi.encodeWithSelector(token.transfer.selector, to, value) + ); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + callOptionalReturn( + token, + abi.encodeWithSelector(token.transferFrom.selector, from, to, value) + ); + } + + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + // solhint-disable-next-line max-line-length + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, value) + ); + } + + function safeIncreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender).add( + value + ); + callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + function safeDecreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender).sub( + value + ); + callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. + + // A Solidity high level call has three parts: + // 1. The target address is checked to verify it contains contract code + // 2. The call itself is made, and success asserted + // 3. The return value is decoded, which in turn checks the size of the returned data. + // solhint-disable-next-line max-line-length + require(address(token).isContract(), "SafeERC20: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = address(token).call(data); + + require(success, "SafeERC20: low-level call failed"); + + if (returndata.length > 0) { + // Return data is optional + // solhint-disable-next-line max-line-length + require( + abi.decode(returndata, (bool)), + "SafeERC20: ERC20 operation did not succeed" + ); + } + } +} + +// https://docs.synthetix.io/contracts/Owned +contract Owned { + address public owner; + address public nominatedOwner; + + constructor(address _owner) public { + require(_owner != address(0), "Owner address cannot be 0"); + owner = _owner; + emit OwnerChanged(address(0), _owner); + } + + function nominateNewOwner(address _owner) external onlyOwner { + nominatedOwner = _owner; + emit OwnerNominated(_owner); + } + + function acceptOwnership() external { + require( + msg.sender == nominatedOwner, + "You must be nominated before you can accept ownership" + ); + emit OwnerChanged(owner, nominatedOwner); + owner = nominatedOwner; + nominatedOwner = address(0); + } + + modifier onlyOwner() { + require( + msg.sender == owner, + "Only the contract owner may perform this action" + ); + _; + } + + event OwnerNominated(address newOwner); + event OwnerChanged(address oldOwner, address newOwner); +} + +// Inheritance + +// https://docs.synthetix.io/contracts/RewardsDistributionRecipient +contract RewardsDistributionRecipient is Owned { + address public rewardsDistribution; + + function notifyRewardAmount( + uint256 rewardNotifyAmount, + uint256 rewardTransferAmount + ) external; + + modifier onlyRewardsDistribution() { + require(msg.sender == rewardsDistribution, "Wrong caller"); + _; + } + + function setRewardsDistribution(address _rewardsDistribution) + external + onlyOwner + { + rewardsDistribution = _rewardsDistribution; + } +} + +contract TokenWrapper is ReentrancyGuard { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + IERC20 public stakingToken; + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + constructor(address _stakingToken) public { + stakingToken = IERC20(_stakingToken); + } + + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function stake(uint256 amount) public nonReentrant { + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) public nonReentrant { + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + stakingToken.safeTransfer(msg.sender, amount); + } +} + +interface IStratAccessNft { + function getTotalUseCount(address _account, uint256 _id) + external + view + returns (uint256); + + function getStratUseCount( + address _account, + uint256 _id, + address _strategy + ) external view returns (uint256); + + function startUsingNFT(address _account, uint256 _id) external; + + function endUsingNFT(address _account, uint256 _id) external; +} + +contract DarkParadise is TokenWrapper, RewardsDistributionRecipient { + IERC20 public rewardsToken; + + uint256 public DURATION = 1 seconds; + + uint256 public periodFinish = 0; + uint256 public rewardRate = 0; + uint256 public lastUpdateTime; + uint256 public rewardPerTokenStored; + mapping(address => uint256) public userRewardPerTokenPaid; + mapping(address => uint256) public rewards; + + //NFT + IStratAccessNft public nft; + + // common, rare, unique ids considered in range on 1-111 + uint256 public constant rareMinId = 101; + uint256 public constant uniqueId = 111; + + uint256 public minNFTId = 223; + uint256 public maxNFTId = 444; + + uint256 public commonLimit = 3200 * 10**18; + uint256 public rareLimit = 16500 * 10**18; + uint256 public uniqueLimit = 30000 * 10**18; + + mapping(address => uint256) public usedNFT; + + event RewardAdded(uint256 rewardNotifyAmount, uint256 rewardTransferAmount); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event RewardPaid(address indexed user, uint256 reward); + event DurationChange(uint256 newDuration, uint256 oldDuration); + + function setDepositLimits( + uint256 _common, + uint256 _rare, + uint256 _unique + ) external onlyOwner { + if (commonLimit != _common) commonLimit = _common; + if (rareLimit != _rare) rareLimit = _rare; + if (uniqueLimit != _unique) uniqueLimit = _unique; + } + + function setMinMaxNFT(uint256 _min, uint256 _max) external onlyOwner { + if (minNFTId != _min) minNFTId = _min; + if (maxNFTId != _max) maxNFTId = _max; + } + + function setDuration(uint256 newDuration) external onlyOwner { + emit DurationChange(newDuration, DURATION); + DURATION = newDuration; + } + + constructor( + address _owner, + address _rewardsToken, + address _stakingToken, + IStratAccessNft _nft + ) public TokenWrapper(_stakingToken) Owned(_owner) { + rewardsToken = IERC20(_rewardsToken); + nft = _nft; + } + + modifier updateReward(address account) { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = lastTimeRewardApplicable(); + if (account != address(0)) { + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + } + _; + } + + function lastTimeRewardApplicable() public view returns (uint256) { + return Math.min(block.timestamp, periodFinish); + } + + function rewardPerToken() public view returns (uint256) { + if (totalSupply() == 0) { + return rewardPerTokenStored; + } + return + rewardPerTokenStored.add( + lastTimeRewardApplicable() + .sub(lastUpdateTime) + .mul(rewardRate) + .mul(1e18) + .div(totalSupply()) + ); + } + + function earned(address account) public view returns (uint256) { + return + balanceOf(account) + .mul(rewardPerToken().sub(userRewardPerTokenPaid[account])) + .div(1e18) + .add(rewards[account]); + } + + function getLimit(address user) public view returns (uint256) { + uint256 nftId = usedNFT[user]; + if (nftId == 0) return 0; + + uint256 effectiveId = ((nftId - 1) % 111) + 1; + if (effectiveId < rareMinId) return commonLimit; + if (effectiveId < uniqueId) return rareLimit; + return uniqueLimit; + } + + // stake visibility is public as overriding LPTokenWrapper's stake() function + function stake(uint256 amount, uint256 _nftId) + public + updateReward(msg.sender) + { + require(amount > 0, "Cannot stake 0"); + require(_nftId >= minNFTId && _nftId <= maxNFTId, "Invalid nft"); + + if (usedNFT[msg.sender] == 0) { + usedNFT[msg.sender] = _nftId; + nft.startUsingNFT(msg.sender, _nftId); + } + + require( + (amount + balanceOf(msg.sender)) <= getLimit(msg.sender), + "Crossing limit" + ); + + super.stake(amount); + emit Staked(msg.sender, amount); + } + + function withdraw(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Cannot withdraw 0"); + + //When a user withdraws their entire SDT from the strat, the strat stops using their NFT + if (balanceOf(msg.sender) - amount == 0) { + uint256 nftId = usedNFT[msg.sender]; + usedNFT[msg.sender] = 0; + nft.endUsingNFT(msg.sender, nftId); + } + super.withdraw(amount); + emit Withdrawn(msg.sender, amount); + } + + function exit() external { + withdraw(balanceOf(msg.sender)); + getReward(); + } + + function getReward() public updateReward(msg.sender) { + uint256 reward = earned(msg.sender); + if (reward > 0) { + rewards[msg.sender] = 0; + rewardsToken.safeTransfer(msg.sender, reward); + emit RewardPaid(msg.sender, reward); + } + } + + function notifyRewardAmount( + uint256 rewardNotifyAmount, + uint256 rewardTransferAmount + ) external onlyRewardsDistribution updateReward(address(0)) { + require(rewardNotifyAmount >= rewardTransferAmount, "!Notify Amount"); + if (block.timestamp >= periodFinish) { + rewardRate = rewardNotifyAmount.div(DURATION); + } else { + uint256 remaining = periodFinish.sub(block.timestamp); + uint256 leftover = remaining.mul(rewardRate); + rewardRate = rewardNotifyAmount.add(leftover).div(DURATION); + } + lastUpdateTime = block.timestamp; + periodFinish = block.timestamp.add(DURATION); + + rewardsToken.safeTransferFrom( + msg.sender, + address(this), + rewardTransferAmount + ); + emit RewardAdded(rewardNotifyAmount, rewardTransferAmount); + } +} diff --git a/nft/contracts/ERC1155Tradable.sol b/nft/contracts/ERC1155Tradable.sol new file mode 100644 index 0000000..fdcc079 --- /dev/null +++ b/nft/contracts/ERC1155Tradable.sol @@ -0,0 +1,1241 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.0; +pragma experimental ABIEncoderV2; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/GSN/Context.sol"; +import "@openzeppelin/contracts/ownership/Ownable.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title Roles + * @dev Library for managing addresses assigned to a Role. + */ +library Roles { + struct Role { + mapping(address => bool) bearer; + } + + /** + * @dev Give an account access to this role. + */ + function add(Role storage role, address account) internal { + require(!has(role, account), "Roles: account already has role"); + role.bearer[account] = true; + } + + /** + * @dev Remove an account's access to this role. + */ + function remove(Role storage role, address account) internal { + require(has(role, account), "Roles: account does not have role"); + role.bearer[account] = false; + } + + /** + * @dev Check if an account has this role. + * @return bool + */ + function has(Role storage role, address account) + internal + view + returns (bool) + { + require(account != address(0), "Roles: account is the zero address"); + return role.bearer[account]; + } +} + +contract MinterRole is Context { + using Roles for Roles.Role; + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + Roles.Role private _minters; + + constructor() internal { + _addMinter(_msgSender()); + } + + modifier onlyMinter() { + require( + isMinter(_msgSender()), + "MinterRole: caller does not have the Minter role" + ); + _; + } + + function isMinter(address account) public view returns (bool) { + return _minters.has(account); + } + + function addMinter(address account) public onlyMinter { + _addMinter(account); + } + + function renounceMinter() public { + _removeMinter(_msgSender()); + } + + function _addMinter(address account) internal { + _minters.add(account); + emit MinterAdded(account); + } + + function _removeMinter(address account) internal { + _minters.remove(account); + emit MinterRemoved(account); + } +} + +/** + * @title WhitelistAdminRole + * @dev WhitelistAdmins are responsible for assigning and removing Whitelisted accounts. + */ +contract WhitelistAdminRole is Context { + using Roles for Roles.Role; + + event WhitelistAdminAdded(address indexed account); + event WhitelistAdminRemoved(address indexed account); + + Roles.Role private _whitelistAdmins; + + constructor() internal { + _addWhitelistAdmin(_msgSender()); + } + + modifier onlyWhitelistAdmin() { + require( + isWhitelistAdmin(_msgSender()), + "WhitelistAdminRole: caller does not have the WhitelistAdmin role" + ); + _; + } + + function isWhitelistAdmin(address account) public view returns (bool) { + return _whitelistAdmins.has(account); + } + + function addWhitelistAdmin(address account) public onlyWhitelistAdmin { + _addWhitelistAdmin(account); + } + + function renounceWhitelistAdmin() public { + _removeWhitelistAdmin(_msgSender()); + } + + function _addWhitelistAdmin(address account) internal { + _whitelistAdmins.add(account); + emit WhitelistAdminAdded(account); + } + + function _removeWhitelistAdmin(address account) internal { + _whitelistAdmins.remove(account); + emit WhitelistAdminRemoved(account); + } +} + +/** + * @title ERC165 + * @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md + */ +interface IERC165 { + /** + * @notice Query if a contract implements an interface + * @dev Interface identification is specified in ERC-165. This function + * uses less than 30,000 gas + * @param _interfaceId The interface identifier, as specified in ERC-165 + */ + function supportsInterface(bytes4 _interfaceId) + external + view + returns (bool); +} + +/** + * @dev ERC-1155 interface for accepting safe transfers. + */ +interface IERC1155TokenReceiver { + /** + * @notice Handle the receipt of a single ERC1155 token type + * @dev An ERC1155-compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeTransferFrom` after the balance has been updated + * This function MAY throw to revert and reject the transfer + * Return of other amount than the magic value MUST result in the transaction being reverted + * Note: The token contract address is always the message sender + * @param _operator The address which called the `safeTransferFrom` function + * @param _from The address which previously owned the token + * @param _id The id of the token being transferred + * @param _amount The amount of tokens being transferred + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155Received( + address _operator, + address _from, + uint256 _id, + uint256 _amount, + bytes calldata _data + ) external returns (bytes4); + + /** + * @notice Handle the receipt of multiple ERC1155 token types + * @dev An ERC1155-compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeBatchTransferFrom` after the balances have been updated + * This function MAY throw to revert and reject the transfer + * Return of other amount than the magic value WILL result in the transaction being reverted + * Note: The token contract address is always the message sender + * @param _operator The address which called the `safeBatchTransferFrom` function + * @param _from The address which previously owned the token + * @param _ids An array containing ids of each token being transferred + * @param _amounts An array containing amounts of each token being transferred + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) external returns (bytes4); + + /** + * @notice Indicates whether a contract implements the `ERC1155TokenReceiver` functions and so can accept ERC1155 token types. + * @param interfaceID The ERC-165 interface ID that is queried for support.s + * @dev This function MUST return true if it implements the ERC1155TokenReceiver interface and ERC-165 interface. + * This function MUST NOT consume more than 5,000 gas. + * @return Wheter ERC-165 or ERC1155TokenReceiver interfaces are supported. + */ + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} + +interface IERC1155 { + // Events + + /** + * @dev Either TransferSingle or TransferBatch MUST emit when tokens are transferred, including zero amount transfers as well as minting or burning + * Operator MUST be msg.sender + * When minting/creating tokens, the `_from` field MUST be set to `0x0` + * When burning/destroying tokens, the `_to` field MUST be set to `0x0` + * The total amount transferred from address 0x0 minus the total amount transferred to 0x0 may be used by clients and exchanges to be added to the "circulating supply" for a given token ID + * To broadcast the existence of a token ID with no initial balance, the contract SHOULD emit the TransferSingle event from `0x0` to `0x0`, with the token creator as `_operator`, and a `_amount` of 0 + */ + event TransferSingle( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256 _id, + uint256 _amount + ); + + /** + * @dev Either TransferSingle or TransferBatch MUST emit when tokens are transferred, including zero amount transfers as well as minting or burning + * Operator MUST be msg.sender + * When minting/creating tokens, the `_from` field MUST be set to `0x0` + * When burning/destroying tokens, the `_to` field MUST be set to `0x0` + * The total amount transferred from address 0x0 minus the total amount transferred to 0x0 may be used by clients and exchanges to be added to the "circulating supply" for a given token ID + * To broadcast the existence of multiple token IDs with no initial balance, this SHOULD emit the TransferBatch event from `0x0` to `0x0`, with the token creator as `_operator`, and a `_amount` of 0 + */ + event TransferBatch( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256[] _ids, + uint256[] _amounts + ); + + /** + * @dev MUST emit when an approval is updated + */ + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + + /** + * @dev MUST emit when the URI is updated for a token ID + * URIs are defined in RFC 3986 + * The URI MUST point a JSON file that conforms to the "ERC-1155 Metadata JSON Schema" + */ + event URI(string _amount, uint256 indexed _id); + + /** + * @notice Transfers amount of an _id from the _from address to the _to address specified + * @dev MUST emit TransferSingle event on success + * Caller must be approved to manage the _from account's tokens (see isApprovedForAll) + * MUST throw if `_to` is the zero address + * MUST throw if balance of sender for token `_id` is lower than the `_amount` sent + * MUST throw on any other error + * When transfer is complete, this function MUST check if `_to` is a smart contract (code size > 0). If so, it MUST call `onERC1155Received` on `_to` and revert if the return amount is not `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes calldata _data + ) external; + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @dev MUST emit TransferBatch event on success + * Caller must be approved to manage the _from account's tokens (see isApprovedForAll) + * MUST throw if `_to` is the zero address + * MUST throw if length of `_ids` is not the same as length of `_amounts` + * MUST throw if any of the balance of sender for token `_ids` is lower than the respective `_amounts` sent + * MUST throw on any other error + * When transfer is complete, this function MUST check if `_to` is a smart contract (code size > 0). If so, it MUST call `onERC1155BatchReceived` on `_to` and revert if the return amount is not `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * Transfers and events MUST occur in the array order they were submitted (_ids[0] before _ids[1], etc) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) external; + + /** + * @notice Get the balance of an account's Tokens + * @param _owner The address of the token holder + * @param _id ID of the Token + * @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) + external + view + returns (uint256); + + /** + * @notice Get the balance of multiple account/token pairs + * @param _owners The addresses of the token holders + * @param _ids ID of the Tokens + * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) + */ + function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) + external + view + returns (uint256[] memory); + + /** + * @notice Enable or disable approval for a third party ("operator") to manage all of caller's tokens + * @dev MUST emit the ApprovalForAll event on success + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + * @notice Queries the approval status of an operator for a given owner + * @param _owner The owner of the Tokens + * @param _operator Address of authorized operator + * @return True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) + external + view + returns (bool isOperator); +} + +/** + * @dev Implementation of Multi-Token Standard contract + */ +contract ERC1155 is IERC165 { + using SafeMath for uint256; + using Address for address; + + /***********************************| + | Variables and Events | + |__________________________________*/ + + // onReceive function signatures + bytes4 internal constant ERC1155_RECEIVED_VALUE = 0xf23a6e61; + bytes4 internal constant ERC1155_BATCH_RECEIVED_VALUE = 0xbc197c81; + + // Objects balances + mapping(address => mapping(uint256 => uint256)) internal balances; + + // Operator Functions + mapping(address => mapping(address => bool)) internal operators; + + // Events + event TransferSingle( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256 _id, + uint256 _amount + ); + event TransferBatch( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256[] _ids, + uint256[] _amounts + ); + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + event URI(string _uri, uint256 indexed _id); + + /***********************************| + | Public Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) public { + require( + (msg.sender == _from) || isApprovedForAll(_from, msg.sender), + "ERC1155#safeTransferFrom: INVALID_OPERATOR" + ); + require( + _to != address(0), + "ERC1155#safeTransferFrom: INVALID_RECIPIENT" + ); + // require(_amount >= balances[_from][_id]) is not necessary since checked with safemath operations + + _safeTransferFrom(_from, _to, _id, _amount); + _callonERC1155Received(_from, _to, _id, _amount, _data); + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public { + // Requirements + require( + (msg.sender == _from) || isApprovedForAll(_from, msg.sender), + "ERC1155#safeBatchTransferFrom: INVALID_OPERATOR" + ); + require( + _to != address(0), + "ERC1155#safeBatchTransferFrom: INVALID_RECIPIENT" + ); + + _safeBatchTransferFrom(_from, _to, _ids, _amounts); + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, _data); + } + + /***********************************| + | Internal Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + */ + function _safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount + ) internal { + // Update balances + balances[_from][_id] = balances[_from][_id].sub(_amount); // Subtract amount + balances[_to][_id] = balances[_to][_id].add(_amount); // Add amount + + // Emit event + emit TransferSingle(msg.sender, _from, _to, _id, _amount); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155Received(...) + */ + function _callonERC1155Received( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) internal { + // Check if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155Received( + msg.sender, + _from, + _id, + _amount, + _data + ); + require( + retval == ERC1155_RECEIVED_VALUE, + "ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE" + ); + } + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + */ + function _safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts + ) internal { + require( + _ids.length == _amounts.length, + "ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH" + ); + + // Number of transfer to execute + uint256 nTransfer = _ids.length; + + // Executing all transfers + for (uint256 i = 0; i < nTransfer; i++) { + // Update storage balance of previous bin + balances[_from][_ids[i]] = balances[_from][_ids[i]].sub( + _amounts[i] + ); + balances[_to][_ids[i]] = balances[_to][_ids[i]].add(_amounts[i]); + } + + // Emit event + emit TransferBatch(msg.sender, _from, _to, _ids, _amounts); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155BatchReceived(...) + */ + function _callonERC1155BatchReceived( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) internal { + // Pass data if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155BatchReceived( + msg.sender, + _from, + _ids, + _amounts, + _data + ); + require( + retval == ERC1155_BATCH_RECEIVED_VALUE, + "ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE" + ); + } + } + + /***********************************| + | Operator Functions | + |__________________________________*/ + + /** + * @notice Enable or disable approval for a third party ("operator") to manage all of caller's tokens + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external { + // Update operator status + operators[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /** + * @notice Queries the approval status of an operator for a given owner + * @param _owner The owner of the Tokens + * @param _operator Address of authorized operator + * @return True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) + public + view + returns (bool isOperator) + { + return operators[_owner][_operator]; + } + + /***********************************| + | Balance Functions | + |__________________________________*/ + + /** + * @notice Get the balance of an account's Tokens + * @param _owner The address of the token holder + * @param _id ID of the Token + * @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) + public + view + returns (uint256) + { + return balances[_owner][_id]; + } + + /** + * @notice Get the balance of multiple account/token pairs + * @param _owners The addresses of the token holders + * @param _ids ID of the Tokens + * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) + */ + function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) + public + view + returns (uint256[] memory) + { + require( + _owners.length == _ids.length, + "ERC1155#balanceOfBatch: INVALID_ARRAY_LENGTH" + ); + + // Variables + uint256[] memory batchBalances = new uint256[](_owners.length); + + // Iterate over each owner and token ID + for (uint256 i = 0; i < _owners.length; i++) { + batchBalances[i] = balances[_owners[i]][_ids[i]]; + } + + return batchBalances; + } + + /***********************************| + | ERC165 Functions | + |__________________________________*/ + + /** + * INTERFACE_SIGNATURE_ERC165 = bytes4(keccak256("supportsInterface(bytes4)")); + */ + bytes4 private constant INTERFACE_SIGNATURE_ERC165 = 0x01ffc9a7; + + /** + * INTERFACE_SIGNATURE_ERC1155 = + * bytes4(keccak256("safeTransferFrom(address,address,uint256,uint256,bytes)")) ^ + * bytes4(keccak256("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")) ^ + * bytes4(keccak256("balanceOf(address,uint256)")) ^ + * bytes4(keccak256("balanceOfBatch(address[],uint256[])")) ^ + * bytes4(keccak256("setApprovalForAll(address,bool)")) ^ + * bytes4(keccak256("isApprovedForAll(address,address)")); + */ + bytes4 private constant INTERFACE_SIGNATURE_ERC1155 = 0xd9b67a26; + + /** + * @notice Query if a contract implements an interface + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` and + */ + function supportsInterface(bytes4 _interfaceID) + external + view + returns (bool) + { + if ( + _interfaceID == INTERFACE_SIGNATURE_ERC165 || + _interfaceID == INTERFACE_SIGNATURE_ERC1155 + ) { + return true; + } + return false; + } +} + +/** + * @notice Contract that handles metadata related methods. + * @dev Methods assume a deterministic generation of URI based on token IDs. + * Methods also assume that URI uses hex representation of token IDs. + */ +contract ERC1155Metadata { + // URI's default URI prefix + string internal baseMetadataURI; + event URI(string _uri, uint256 indexed _id); + + /***********************************| + | Metadata Public Function s | + |__________________________________*/ + + /** + * @notice A distinct Uniform Resource Identifier (URI) for a given token. + * @dev URIs are defined in RFC 3986. + * URIs are assumed to be deterministically generated based on token ID + * Token IDs are assumed to be represented in their hex format in URIs + * @return URI string + */ + function uri(uint256 _id) public view returns (string memory) { + return + string(abi.encodePacked(baseMetadataURI, _uint2str(_id), ".json")); + } + + /***********************************| + | Metadata Internal Functions | + |__________________________________*/ + + /** + * @notice Will emit default URI log event for corresponding token _id + * @param _tokenIDs Array of IDs of tokens to log default URI + */ + function _logURIs(uint256[] memory _tokenIDs) internal { + string memory baseURL = baseMetadataURI; + string memory tokenURI; + + for (uint256 i = 0; i < _tokenIDs.length; i++) { + tokenURI = string( + abi.encodePacked(baseURL, _uint2str(_tokenIDs[i]), ".json") + ); + emit URI(tokenURI, _tokenIDs[i]); + } + } + + /** + * @notice Will emit a specific URI log event for corresponding token + * @param _tokenIDs IDs of the token corresponding to the _uris logged + * @param _URIs The URIs of the specified _tokenIDs + */ + function _logURIs(uint256[] memory _tokenIDs, string[] memory _URIs) + internal + { + require( + _tokenIDs.length == _URIs.length, + "ERC1155Metadata#_logURIs: INVALID_ARRAYS_LENGTH" + ); + for (uint256 i = 0; i < _tokenIDs.length; i++) { + emit URI(_URIs[i], _tokenIDs[i]); + } + } + + /** + * @notice Will update the base URL of token's URI + * @param _newBaseMetadataURI New base URL of token's URI + */ + function _setBaseMetadataURI(string memory _newBaseMetadataURI) internal { + baseMetadataURI = _newBaseMetadataURI; + } + + /***********************************| + | Utility Internal Functions | + |__________________________________*/ + + /** + * @notice Convert uint256 to string + * @param _i Unsigned integer to convert to string + */ + function _uint2str(uint256 _i) + internal + pure + returns (string memory _uintAsString) + { + if (_i == 0) { + return "0"; + } + + uint256 j = _i; + uint256 ii = _i; + uint256 len; + + // Get number of bytes + while (j != 0) { + len++; + j /= 10; + } + + bytes memory bstr = new bytes(len); + uint256 k = len - 1; + + // Get each individual ASCII + while (ii != 0) { + bstr[k--] = bytes1(uint8(48 + (ii % 10))); + ii /= 10; + } + + // Convert to string + return string(bstr); + } +} + +/** + * @dev Multi-Fungible Tokens with minting and burning methods. These methods assume + * a parent contract to be executed as they are `internal` functions + */ +contract ERC1155MintBurn is ERC1155 { + /****************************************| + | Minting Functions | + |_______________________________________*/ + + /** + * @notice Mint _amount of tokens of a given id + * @param _to The address to mint tokens to + * @param _id Token id to mint + * @param _amount The amount to be minted + * @param _data Data to pass if receiver is contract + */ + function _mint( + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) internal { + // Add _amount + balances[_to][_id] = balances[_to][_id].add(_amount); + + // Emit event + emit TransferSingle(msg.sender, address(0x0), _to, _id, _amount); + + // Calling onReceive method if recipient is contract + _callonERC1155Received(address(0x0), _to, _id, _amount, _data); + } + + /** + * @notice Mint tokens for each ids in _ids + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function _batchMint( + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) internal { + require( + _ids.length == _amounts.length, + "ERC1155MintBurn#batchMint: INVALID_ARRAYS_LENGTH" + ); + + // Number of mints to execute + uint256 nMint = _ids.length; + + // Executing all minting + for (uint256 i = 0; i < nMint; i++) { + // Update storage balance + balances[_to][_ids[i]] = balances[_to][_ids[i]].add(_amounts[i]); + } + + // Emit batch mint event + emit TransferBatch(msg.sender, address(0x0), _to, _ids, _amounts); + + // Calling onReceive method if recipient is contract + _callonERC1155BatchReceived(address(0x0), _to, _ids, _amounts, _data); + } + + /****************************************| + | Burning Functions | + |_______________________________________*/ + + /** + * @notice Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function _burn( + address _from, + uint256 _id, + uint256 _amount + ) internal { + //Substract _amount + balances[_from][_id] = balances[_from][_id].sub(_amount); + + // Emit event + emit TransferSingle(msg.sender, _from, address(0x0), _id, _amount); + } + + /** + * @notice Burn tokens of given token id for each (_ids[i], _amounts[i]) pair + * @param _from The address to burn tokens from + * @param _ids Array of token ids to burn + * @param _amounts Array of the amount to be burned + */ + function _batchBurn( + address _from, + uint256[] memory _ids, + uint256[] memory _amounts + ) internal { + require( + _ids.length == _amounts.length, + "ERC1155MintBurn#batchBurn: INVALID_ARRAYS_LENGTH" + ); + + // Number of mints to execute + uint256 nBurn = _ids.length; + + // Executing all minting + for (uint256 i = 0; i < nBurn; i++) { + // Update storage balance + balances[_from][_ids[i]] = balances[_from][_ids[i]].sub( + _amounts[i] + ); + } + + // Emit batch mint event + emit TransferBatch(msg.sender, _from, address(0x0), _ids, _amounts); + } +} + +library Strings { + // via https://github.com/oraclize/ethereum-api/blob/master/oraclizeAPI_0.5.sol + function strConcat( + string memory _a, + string memory _b, + string memory _c, + string memory _d, + string memory _e + ) internal pure returns (string memory) { + bytes memory _ba = bytes(_a); + bytes memory _bb = bytes(_b); + bytes memory _bc = bytes(_c); + bytes memory _bd = bytes(_d); + bytes memory _be = bytes(_e); + string memory abcde = new string( + _ba.length + _bb.length + _bc.length + _bd.length + _be.length + ); + bytes memory babcde = bytes(abcde); + uint256 k = 0; + for (uint256 i = 0; i < _ba.length; i++) babcde[k++] = _ba[i]; + for (uint256 i = 0; i < _bb.length; i++) babcde[k++] = _bb[i]; + for (uint256 i = 0; i < _bc.length; i++) babcde[k++] = _bc[i]; + for (uint256 i = 0; i < _bd.length; i++) babcde[k++] = _bd[i]; + for (uint256 i = 0; i < _be.length; i++) babcde[k++] = _be[i]; + return string(babcde); + } + + function strConcat( + string memory _a, + string memory _b, + string memory _c, + string memory _d + ) internal pure returns (string memory) { + return strConcat(_a, _b, _c, _d, ""); + } + + function strConcat( + string memory _a, + string memory _b, + string memory _c + ) internal pure returns (string memory) { + return strConcat(_a, _b, _c, "", ""); + } + + function strConcat(string memory _a, string memory _b) + internal + pure + returns (string memory) + { + return strConcat(_a, _b, "", "", ""); + } + + function uint2str(uint256 _i) + internal + pure + returns (string memory _uintAsString) + { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len - 1; + while (_i != 0) { + bstr[k--] = bytes1(uint8(48 + (_i % 10))); + _i /= 10; + } + return string(bstr); + } +} + +contract OwnableDelegateProxy {} + +contract ProxyRegistry { + mapping(address => OwnableDelegateProxy) public proxies; +} + +/** + * @title ERC1155Tradable + * ERC1155Tradable - ERC1155 contract that whitelists an operator address, + * has create and mint functionality, and supports useful standards from OpenZeppelin, + like _exists(), name(), symbol(), and totalSupply() + */ +contract ERC1155Tradable is + ERC1155, + ERC1155MintBurn, + ERC1155Metadata, + Ownable, + MinterRole, + WhitelistAdminRole +{ + using Strings for string; + + address proxyRegistryAddress; + uint256 internal _currentTokenID = 0; + mapping(uint256 => address) public creators; + mapping(uint256 => uint256) public tokenSupply; + mapping(uint256 => uint256) public tokenMaxSupply; + // Contract name + string public name; + // Contract symbol + string public symbol; + + constructor( + string memory _name, + string memory _symbol, + address _proxyRegistryAddress + ) public { + name = _name; + symbol = _symbol; + proxyRegistryAddress = _proxyRegistryAddress; + } + + function removeWhitelistAdmin(address account) public onlyOwner { + _removeWhitelistAdmin(account); + } + + function removeMinter(address account) public onlyOwner { + _removeMinter(account); + } + + function uri(uint256 _id) public view returns (string memory) { + require(_exists(_id), "ERC721Tradable#uri: NONEXISTENT_TOKEN"); + return Strings.strConcat(baseMetadataURI, Strings.uint2str(_id)); + } + + /** + * @dev Returns the total quantity for a token ID + * @param _id uint256 ID of the token to query + * @return amount of token in existence + */ + function totalSupply(uint256 _id) public view returns (uint256) { + return tokenSupply[_id]; + } + + /** + * @dev Returns the max quantity for a token ID + * @param _id uint256 ID of the token to query + * @return amount of token in existence + */ + function maxSupply(uint256 _id) public view returns (uint256) { + return tokenMaxSupply[_id]; + } + + /** + * @dev Will update the base URL of token's URI + * @param _newBaseMetadataURI New base URL of token's URI + */ + function setBaseMetadataURI(string memory _newBaseMetadataURI) + public + onlyWhitelistAdmin + { + _setBaseMetadataURI(_newBaseMetadataURI); + } + + /** + * @dev Creates a new token type and assigns _initialSupply to an address + * @param _maxSupply max supply allowed + * @param _initialSupply Optional amount to supply the first owner + * @param _uri Optional URI for this token type + * @param _data Optional data to pass if receiver is contract + * @return The newly created token ID + */ + function create( + uint256 _maxSupply, + uint256 _initialSupply, + string memory _uri, + bytes memory _data + ) public onlyWhitelistAdmin returns (uint256 tokenId) { + require(_initialSupply <= _maxSupply, "_initialSupply > _maxSupply"); + uint256 _id = _getNextTokenID(); + _incrementTokenTypeId(); + creators[_id] = msg.sender; + + if (bytes(_uri).length > 0) { + emit URI(_uri, _id); + } + + if (_initialSupply != 0) _mint(msg.sender, _id, _initialSupply, _data); + tokenSupply[_id] = _initialSupply; + tokenMaxSupply[_id] = _maxSupply; + return _id; + } + + /** + * @dev Creates multiple new token types and assigns _initialSupply[i] of each token type, to an address + * @param _maxSupply Array of max supplies allowed + * @param _initialSupply Array of optional amounts to supply the first owner + * @param _uri Array of optional URIs for each token type + * @param _data Optional data to pass if receiver is contract. Same for each new token type + * @return Array of newly created token IDs + */ + function batchCreate( + uint256[] calldata _maxSupply, + uint256[] calldata _initialSupply, + string[] calldata _uri, + bytes calldata _data + ) external onlyWhitelistAdmin returns (uint256[] memory) { + require( + _initialSupply.length == _maxSupply.length && + _uri.length == _maxSupply.length, + "Array lengths mismatch" + ); + + uint256[] memory _ids = new uint256[](_maxSupply.length); + uint256 _id = 0; + + for (uint256 index = 0; index < _maxSupply.length; index++) { + _id = create( + _maxSupply[index], + _initialSupply[index], + _uri[index], + _data + ); + + _ids[index] = _id; + } + return _ids; + } + + /** + * @dev Mints some amount of tokens to an address + * @param _to Address of the future owner of the token + * @param _id Token ID to mint + * @param _quantity Amount of tokens to mint + * @param _data Data to pass if receiver is contract + */ + function mint( + address _to, + uint256 _id, + uint256 _quantity, + bytes memory _data + ) public onlyMinter { + uint256 tokenId = _id; + require( + tokenSupply[tokenId] < tokenMaxSupply[tokenId], + "Max supply reached" + ); + _mint(_to, _id, _quantity, _data); + tokenSupply[_id] = tokenSupply[_id].add(_quantity); + } + + /** + * @dev Mints some amount of tokens to an address + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function batchMint( + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public onlyMinter { + uint256 nMints = _ids.length; + for (uint256 i = 0; i < nMints; i++) { + uint256 tokenId = _ids[i]; + require( + tokenSupply[tokenId] < tokenMaxSupply[tokenId], + "Max supply reached" + ); + tokenSupply[tokenId] = tokenSupply[tokenId].add(_amounts[i]); + } + _batchMint(_to, _ids, _amounts, _data); + } + + /** + * @notice Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function burn( + address _from, + uint256 _id, + uint256 _amount + ) public onlyOwner { + tokenSupply[_id] = tokenSupply[_id].sub(_amount); + _burn(_from, _id, _amount); + } + + /** + * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-free listings. + */ + function isApprovedForAll(address _owner, address _operator) + public + view + returns (bool isOperator) + { + // Whitelist OpenSea proxy contract for easy trading. + ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); + if (address(proxyRegistry.proxies(_owner)) == _operator) { + return true; + } + + return ERC1155.isApprovedForAll(_owner, _operator); + } + + /** + * @dev Returns whether the specified token exists by checking to see if it has a creator + * @param _id uint256 ID of the token to query the existence of + * @return bool whether the token exists + */ + function _exists(uint256 _id) internal view returns (bool) { + return creators[_id] != address(0); + } + + /** + * @dev calculates the next token ID based on value of _currentTokenID + * @return uint256 for the next token ID + */ + function _getNextTokenID() private view returns (uint256) { + return _currentTokenID.add(1); + } + + /** + * @dev increments the value of _currentTokenID + */ + function _incrementTokenTypeId() private { + _currentTokenID++; + } +} diff --git a/nft/contracts/StakeDaoNFTPalace.sol b/nft/contracts/StakeDaoNFTPalace.sol new file mode 100644 index 0000000..05963fe --- /dev/null +++ b/nft/contracts/StakeDaoNFTPalace.sol @@ -0,0 +1,119 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.0; + +import "./ERC1155Tradable.sol"; + +contract StakeTokenWrapper { + using SafeMath for uint256; + IERC20 public xsdt; + + constructor(IERC20 _xsdt) public { + xsdt = IERC20(_xsdt); + } + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function stake(uint256 amount) public { + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + xsdt.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) public { + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + xsdt.transfer(msg.sender, amount); + } +} + +contract StakeDaoNFTPalace is StakeTokenWrapper, Ownable { + ERC1155Tradable public nft; + + mapping(address => uint256) public lastUpdateTime; + mapping(address => uint256) public points; + mapping(uint256 => uint256) public cards; + + event CardAdded(uint256 card, uint256 points); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event Redeemed(address indexed user, uint256 id); + event NFTSet(ERC1155Tradable indexed newNFT); + + modifier updateReward(address account) { + if (account != address(0)) { + points[account] = earned(account); + lastUpdateTime[account] = block.timestamp; + } + _; + } + + constructor(ERC1155Tradable _nftAddress, IERC20 _xsdt) + public + StakeTokenWrapper(_xsdt) + { + nft = _nftAddress; + } + + function setNFT(ERC1155Tradable _nftAddress) public onlyOwner { + nft = _nftAddress; + emit NFTSet(_nftAddress); + } + + function addCard(uint256 cardId, uint256 amount) public onlyOwner { + cards[cardId] = amount; + emit CardAdded(cardId, amount); + } + + function earned(address account) public view returns (uint256) { + uint256 timeDifference = block.timestamp.sub(lastUpdateTime[account]); + uint256 balance = balanceOf(account); + uint256 decimals = 1e18; + uint256 x = balance / decimals; + uint256 ratePerMin = decimals.mul(x).div(x.add(12000)).div(240); + return points[account].add(ratePerMin.mul(timeDifference)); + } + + // stake visibility is public as overriding StakeTokenWrapper's stake() function + function stake(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Invalid amount"); + + super.stake(amount); + emit Staked(msg.sender, amount); + } + + function withdraw(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Cannot withdraw 0"); + + super.withdraw(amount); + emit Withdrawn(msg.sender, amount); + } + + function exit() external { + withdraw(balanceOf(msg.sender)); + } + + function redeem(uint256 card) public updateReward(msg.sender) { + require(cards[card] != 0, "Card not found"); + require( + points[msg.sender] >= cards[card], + "Not enough points to redeem for card" + ); + require( + nft.totalSupply(card) < nft.maxSupply(card), + "Max cards minted" + ); + + points[msg.sender] = points[msg.sender].sub(cards[card]); + nft.mint(msg.sender, card, 1, ""); + emit Redeemed(msg.sender, card); + } +} diff --git a/nft/contracts/StakeDaoNFT_V2.sol b/nft/contracts/StakeDaoNFT_V2.sol new file mode 100644 index 0000000..b3aedfe --- /dev/null +++ b/nft/contracts/StakeDaoNFT_V2.sol @@ -0,0 +1,202 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.5.0; +pragma experimental ABIEncoderV2; +import "@openzeppelin/contracts/ownership/Ownable.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/GSN/Context.sol"; +import "./ERC1155Tradable.sol"; + +/** + * @title StrategyRole + * @dev Owner is responsible to add/remove strategy + */ +contract StrategyRole is Context, Ownable { + using Roles for Roles.Role; + + event StrategyAdded(address indexed account); + event StrategyRemoved(address indexed account); + + Roles.Role private _strategies; + + modifier onlyStrategy() { + require( + isStrategy(_msgSender()), + "StrategyRole: caller does not have the Strategy role" + ); + _; + } + + function isStrategy(address account) public view returns (bool) { + return _strategies.has(account); + } + + function addStrategy(address account) public onlyOwner { + _addStrategy(account); + } + + function removeStrategy(address account) public onlyOwner { + _removeStrategy(account); + } + + function _addStrategy(address account) internal { + _strategies.add(account); + emit StrategyAdded(account); + } + + function _removeStrategy(address account) internal { + _strategies.remove(account); + emit StrategyRemoved(account); + } +} + +/** + * @title Strategy Access NFT Contract for StakeDAO + * @dev The contract keeps a count of NFTs being used in some strategy for + * for each user and allows transfers based on that. + */ +contract StakeDaoNFT_V2 is ERC1155Tradable, StrategyRole { + using SafeMath for uint256; + + event StartedUsingNFT( + address indexed account, + uint256 indexed id, + address indexed strategy + ); + event EndedUsingNFT( + address indexed account, + uint256 indexed id, + address indexed strategy + ); + + // mapping account => nftId => useCount + // this is used to restrict transfers if nft is being used in any strategy + mapping(address => mapping(uint256 => uint256)) internal totalUseCount; + + // mapping account => nftId => strategyAddress => useCount + // this is used to make sure a strategy can only end using nft that it started using before + mapping(address => mapping(uint256 => mapping(address => uint256))) + internal stratUseCount; + + // TODO: proper name, metadata uri + constructor(address _proxyRegistryAddress) + public + ERC1155Tradable("Stake DAO NFT", "sdNFT", _proxyRegistryAddress) + { + _setBaseMetadataURI( + "https://gateway.pinata.cloud/ipfs/QmWwvdZPT7jPFu5UdUD9t5XFW8SaeTiFQKwNtrmDrqHVTX/metadata/" + ); + + // starting ids for these nfts from 223 + // since 222 nfts (tempest, pythia) have been minted using old implementation + _currentTokenID = 222; + } + + function contractURI() public view returns (string memory) { + return + "https://gateway.pinata.cloud/ipfs/Qmc1i37KPdg7Cp8rzjgp3QoCECaEbfoSymCpKG8hF85ENv"; + } + + function getTotalUseCount(address _account, uint256 _id) + public + view + returns (uint256) + { + return totalUseCount[_account][_id]; + } + + function getStratUseCount( + address _account, + uint256 _id, + address _strategy + ) public view returns (uint256) { + return stratUseCount[_account][_id][_strategy]; + } + + /** + * @notice Mark NFT as being used. Only callable by registered strategies + * @param _account User account address + * @param _id ID of the token type + */ + function startUsingNFT(address _account, uint256 _id) public onlyStrategy { + require( + balances[_account][_id] > 0, + "StakeDaoNFT_V2: user account doesnt have NFT" + ); + stratUseCount[_account][_id][msg.sender] = stratUseCount[_account][_id][ + msg.sender + ].add(1); + totalUseCount[_account][_id] = totalUseCount[_account][_id].add(1); + emit StartedUsingNFT(_account, _id, msg.sender); + } + + /** + * @notice Unmark NFT as being used. Only callable by registered strategies + * @param _account User account address + * @param _id ID of the token type + */ + function endUsingNFT(address _account, uint256 _id) public onlyStrategy { + // if a strategy tries to call endUsingNFT function for which it did not call + // startUsingNFT then subtraction reverts due to safemath. + stratUseCount[_account][_id][msg.sender] = stratUseCount[_account][_id][ + msg.sender + ].sub(1); + totalUseCount[_account][_id] = totalUseCount[_account][_id].sub(1); + emit EndedUsingNFT(_account, _id, msg.sender); + } + + /** + * @dev Overrides safeTransferFrom function of ERC1155 to introduce totalUseCount check + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) public { + // check if nft is being used + require( + totalUseCount[_from][_id] == 0, + "StakeDaoNFT_V2: NFT being used in strategy" + ); + super.safeTransferFrom(_from, _to, _id, _amount, _data); + } + + /** + * @dev Overrides safeBatchTransferFrom function of ERC1155 to introduce totalUseCount check + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public { + // Number of transfer to execute + uint256 nTransfer = _ids.length; + + // check if any nft is being used + for (uint256 i = 0; i < nTransfer; i++) { + require( + totalUseCount[_from][_ids[i]] == 0, + "StakeDaoNFT_V2: NFT being used in strategy" + ); + } + + super.safeBatchTransferFrom(_from, _to, _ids, _amounts, _data); + } + + function burn( + address _from, + uint256 _id, + uint256 _amount + ) public onlyOwner { + // check if nft is being used + require( + totalUseCount[_from][_id] == 0, + "StakeDaoNFT_V2: NFT being used in strategy" + ); + super.burn(_from, _id, _amount); + } +} diff --git a/nft/hardhat.config.ts b/nft/hardhat.config.ts new file mode 100644 index 0000000..1568a42 --- /dev/null +++ b/nft/hardhat.config.ts @@ -0,0 +1,47 @@ +import { HardhatUserConfig } from 'hardhat/config' +import '@nomiclabs/hardhat-ethers' +import '@nomiclabs/hardhat-waffle' +import '@nomiclabs/hardhat-etherscan' +import 'hardhat-tracer' + +import './tasks/deploy-usd' +import './tasks/deploy-btc' +import './tasks/deploy-eur' +import './tasks/deploy-frx' +import './tasks/deploy-stratNFT' +import './tasks/create-nfts' + +require('dotenv').config() + +const DEPLOYER = process.env.DEPLOYER + +export default { + defaultNetwork: 'hardhat', + networks: { + hardhat: { + forking: { + url: process.env.ALCHEMY_MAINNET, + blockNumber: 13250958, + }, + }, + mainnet: { + url: process.env.ALCHEMY_MAINNET, + accounts: [`0x${DEPLOYER}`], + }, + rinkeby: { + url: process.env.ALCHEMY_RINKEBY, + accounts: [process.env.ACCOUNTRINKEBY], + }, + }, + solidity: { + compilers: [ + { version: '0.8.0' }, + { version: '0.7.4' }, + { version: '0.6.12' }, + { version: '0.5.17' }, + ], + }, + etherscan: { + apiKey: process.env.ETHERSCAN_KEY, + }, +} as HardhatUserConfig diff --git a/nft/package.json b/nft/package.json new file mode 100644 index 0000000..dcc722a --- /dev/null +++ b/nft/package.json @@ -0,0 +1,35 @@ +{ + "name": "convex-test", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@nomiclabs/hardhat-etherscan": "^2.1.2", + "@nomiclabs/hardhat-waffle": "^2.0.0", + "@types/chai": "^4.2.17", + "@types/mocha": "^8.2.2", + "@types/node": "^15.0.1", + "chai": "^4.3.4", + "ethereum-waffle": "^3.0.0", + "ethers": "^5.0.0", + "hardhat": "^2.2.1", + "hardhat-abi-exporter": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.10", + "ts-node": "^9.1.1", + "typescript": "^4.2.4" + }, + "dependencies": { + "@0xsequence/erc-1155": "^3.0.4", + "@chainlink/contracts": "^0.1.7", + "@ethersproject/units": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@openzeppelin/contracts": "2.5.1", + "@uniswap/v3-periphery": "^1.1.0", + "dotenv": "^8.2.0", + "hardhat-tracer": "1.0.0-alpha.6", + "prettier": "^2.3.0" + }, + "scripts": { + "prettier": "prettier --write 'contracts/**/*.sol'" + } +} diff --git a/nft/readme.md b/nft/readme.md new file mode 100644 index 0000000..9e92ae2 --- /dev/null +++ b/nft/readme.md @@ -0,0 +1,3 @@ +## Run test + +`npx hardhat test test/strategy.ts` diff --git a/nft/test/contracturi.json b/nft/test/contracturi.json new file mode 100644 index 0000000..ad7d836 --- /dev/null +++ b/nft/test/contracturi.json @@ -0,0 +1,6 @@ +{ + "name": "Dum DAO", + "description": "Strengths in dummy", + "external_link": "https://google.com", + "image": "https://static.turbosquid.com/Preview/2019/02/18__04_59_25/Crash_Test_Dummy_Rigged_c4d_00.jpg06869F8F-4BC5-4818-AAAD-72A1BCBF72C5Large.jpg" +} \ No newline at end of file diff --git a/nft/test/strategyArb.ts b/nft/test/strategyArb.ts new file mode 100644 index 0000000..31f65b9 --- /dev/null +++ b/nft/test/strategyArb.ts @@ -0,0 +1,1363 @@ +import { ethers, network } from 'hardhat' +import { expect } from 'chai' +import { Contract } from '@ethersproject/contracts' +import { parseEther } from '@ethersproject/units' +import { JsonRpcSigner } from '@ethersproject/providers' + +import ERC20 from '../abis/ERC20.json' +import Palace from '../abis/Palace.json' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +const SDT = '0x73968b9a57c6e53d41345fd57a6e6ae27d6cdb2f' +const sdFRAX3CRV = '0x5af15DA84A4a6EDf2d9FA6720De921E1026E37b7' +const PALACE = '0x221738f73fA4bfCA91918E77d112b87D918c751f' + +const SDTWHALE = '0xc5d3d004a223299c4f95bb702534c14a32e8778c' +const sdFRAX3CRVWHALE = '0x285e4f019a531e20f673b634d31922d408970798' +const DEPLOYER = '0xb36a0671B3D49587236d7833B01E79798175875f' + +const DUMMYUSERC = '0x80d9BC4B2B21C69ba2B7ED92882fF79069Ea7e13' +const PALACE_WHALE = '0x6d75fFBfFd1e63e5072F0Ffbf6c4EeFa16043967' + +const commonLimit = parseEther('1000') +const rareLimit = parseEther('5000') +const uniqueLimit = parseEther('10000') + +describe('Arb Strategy', function () { + let stakingRewards: Contract + let sdt: Contract + let sdFRAX3CRVContract: Contract + let stratAccessNFTContract: Contract + let palace: Contract + let sdtWhaleSigner: JsonRpcSigner + let sdFrax3CRVWhaleSigner: JsonRpcSigner + let baseOwner: SignerWithAddress + let deployerSigner: JsonRpcSigner + let palaceWhaleSigner: JsonRpcSigner + let userC: JsonRpcSigner + + before(async function () { + this.enableTimeouts(false) + const [owner] = await ethers.getSigners() + baseOwner = owner + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [SDTWHALE], + }) + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [sdFRAX3CRVWHALE], + }) + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [DEPLOYER], + }) + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [DUMMYUSERC], + }) + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [PALACE_WHALE], + }) + + sdt = await ethers.getContractAt(ERC20, SDT) + palace = await ethers.getContractAt(Palace, PALACE) + sdFRAX3CRVContract = await ethers.getContractAt(ERC20, sdFRAX3CRV) + sdtWhaleSigner = await ethers.provider.getSigner(SDTWHALE) + sdFrax3CRVWhaleSigner = await ethers.provider.getSigner(sdFRAX3CRVWHALE) + deployerSigner = await ethers.provider.getSigner(DEPLOYER) + palaceWhaleSigner = await ethers.provider.getSigner(PALACE_WHALE) + userC = await ethers.provider.getSigner(DUMMYUSERC) + + await owner.sendTransaction({ + to: DEPLOYER, + value: parseEther('100').toHexString(), + }) + + const StratAccessNFT = await ethers.getContractFactory('StakeDaoNFT_V2') + stratAccessNFTContract = await StratAccessNFT.deploy( + '0xa5409ec958C83C3f309868babACA7c86DCB077c1', + ) + + const temp = await owner.sendTransaction({ + to: SDTWHALE, + value: parseEther('100').toHexString(), + }) + + await owner.sendTransaction({ + to: sdFRAX3CRVWHALE, + value: parseEther('100').toHexString(), + }) + + await owner.sendTransaction({ + to: DUMMYUSERC, + value: parseEther('100').toHexString(), + }) + + await sdt + .connect(sdtWhaleSigner) + .transfer(owner.getAddress(), parseEther('10000')) + await sdt.connect(sdtWhaleSigner).transfer(DUMMYUSERC, parseEther('320')) + await sdFRAX3CRVContract + .connect(sdFrax3CRVWhaleSigner) + .transfer(deployerSigner.getAddress(), parseEther('900')) + + await sdt.connect(sdtWhaleSigner).transfer(PALACE_WHALE, parseEther('5000')) + + // await sdFRAX3CRVContract + // .connect(sdFrax3CRVWhaleSigner) + // .transfer(baseOwner.address, parseEther('100')) + await stratAccessNFTContract.create(1, 1, 'yello', '0x') + await stratAccessNFTContract.create(1, 1, 'yello', '0x') + await stratAccessNFTContract.create(1, 1, 'yello', '0x') + + for (let index = 0; index < 109; index++) { + await stratAccessNFTContract.create(1, 0, 'yello', '0x') + } + + await stratAccessNFTContract.addMinter(palace.address) + + await palace.connect(deployerSigner).setNFT(stratAccessNFTContract.address) + // deployer adding Cards (registering NFTs) + await palace.connect(deployerSigner).addCard(230, parseEther('46000')) + // then need a test wherein one of the palace holders (who has more than 48000 points earned in this case) + // is able to mint from palace + + await stratAccessNFTContract.safeTransferFrom( + owner.getAddress(), + SDTWHALE, + 224, + '1', + '0x', + ) + + await stratAccessNFTContract.safeTransferFrom( + owner.getAddress(), + DUMMYUSERC, + 225, + '1', + '0x', + ) + }) + + async function InitializeStakingRewards() { + const StakingRewards = await ethers.getContractFactory('DarkParadise') + stakingRewards = await StakingRewards.connect(deployerSigner).deploy( + DEPLOYER, + sdFRAX3CRV, + SDT, + stratAccessNFTContract.address, + ) + await stratAccessNFTContract.addStrategy(stakingRewards.address) + await stakingRewards + .connect(deployerSigner) + .setRewardsDistribution(DEPLOYER) + } + + describe('NFT Test', async () => { + it('Batch', async function () { + var max = [] + var initSupp = [] + var uri = [] + for (let index = 0; index < 223; index++) { + max[index] = index + initSupp[index] = index + uri[index] = '' + } + await stratAccessNFTContract.batchCreate(max, initSupp, uri, '0x') + + for (let index = 223; index < 445; index++) { + expect(await stratAccessNFTContract.creators(index)).to.equal( + baseOwner.address, + ) + } + }) + }) + + describe('Staking', async () => { + beforeEach(async function () { + await InitializeStakingRewards() + }) + + it('User could not stake with any nft', async function () { + await sdt.approve(stakingRewards.address, parseEther('1')) + await expect( + stakingRewards['stake(uint256,uint256)'](parseEther('1'), 1), + ).revertedWith('Invalid nft') + }) + + it('User could stake only if they have nft', async function () { + await sdt.approve(stakingRewards.address, parseEther('1')) + + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + expect(await sdt.balanceOf(stakingRewards.address)).to.equal( + parseEther('1'), + ) + }) + + it('User could not transfer a NFT that is staked in a strategy', async function () { + await sdt.approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + + await expect( + stratAccessNFTContract.safeTransferFrom( + baseOwner.getAddress(), + SDTWHALE, + 223, + '1', + '0x', + ), + ).revertedWith('StakeDaoNFT_V2: NFT being used in strategy') + }) + + it('User could not stake if they do not have nft', async function () { + await sdt + .connect(sdtWhaleSigner) + .approve(stakingRewards.address, parseEther('1')) + await expect( + stakingRewards + .connect(sdtWhaleSigner) + ['stake(uint256,uint256)'](parseEther('1'), 225), + ).revertedWith('StakeDaoNFT_V2: user account doesnt have NFT') + }) + + it('User should be able to deposit the amount their NFT permits', async function () { + await sdt.approve(stakingRewards.address, commonLimit) + expect(await sdt.balanceOf(baseOwner.getAddress())).to.equal( + parseEther('9998'), + ) + await expect( + stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](commonLimit, 223), + ).emit(stakingRewards, 'Staked') + expect(await sdt.balanceOf(stakingRewards.address)).to.equal(commonLimit) + expect(await sdt.balanceOf(baseOwner.getAddress())).to.equal( + parseEther('8998'), + ) + }) + + it('User should not be able to deposit more than the amount their NFT permits', async function () { + await sdt.approve(stakingRewards.address, uniqueLimit) + await expect( + stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](uniqueLimit, 223), + ).revertedWith('Crossing limit') + }) + + it('Palace whale is able to mint from palace', async function () { + await palace.connect(palaceWhaleSigner).redeem(230) + const nftBal = await stratAccessNFTContract.balanceOf(PALACE_WHALE, 230) + await expect(nftBal).to.equal(1) + }) + }) + + describe('Withdrawing', async () => { + beforeEach(async function () { + await InitializeStakingRewards() + await sdt.approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + }) + + it('User could withdraw complete amount', async function () { + expect(await sdt.balanceOf(baseOwner.getAddress())).to.equal( + parseEther('8997'), + ) + await stakingRewards.connect(baseOwner).exit() + expect(await sdt.balanceOf(stakingRewards.address)).to.equal( + parseEther('0'), + ) + expect(await sdt.balanceOf(baseOwner.getAddress())).to.equal( + parseEther('8998'), + ) + }) + + it('User could withdraw partial amount', async function () { + expect(await sdt.balanceOf(baseOwner.getAddress())).to.equal( + parseEther('8997'), + ) + await stakingRewards.connect(baseOwner).withdraw(parseEther('0.5')) + expect(await sdt.balanceOf(stakingRewards.address)).to.equal( + parseEther('0.5'), + ) + expect(await sdt.balanceOf(baseOwner.getAddress())).to.equal( + parseEther('8997.5'), + ) + }) + + it('User cannot withdraw amount more than they have staked', async function () { + await expect(stakingRewards.withdraw(parseEther('2'))).revertedWith( + 'SafeMath: subtraction overflow', + ) + }) + + it('User cannot withdraw if they dont have any amount staked', async function () { + await expect( + stakingRewards.connect(sdtWhaleSigner).withdraw(parseEther('1')), + ).revertedWith('SafeMath: subtraction overflow') + }) + }) + + describe('Rewards', async () => { + describe('Deposit', async () => { + beforeEach(async function () { + await InitializeStakingRewards() + await sdFRAX3CRVContract + .connect(deployerSigner) + .approve(stakingRewards.address, parseEther('10')) + }) + + it('Only authorized user could deposit rewards', async function () { + await stakingRewards + .connect(deployerSigner) + .notifyRewardAmount(parseEther('1'), parseEther('1')) + }) + + it('Unauthorized user could not deposit rewards', async function () { + await expect( + stakingRewards + .connect(sdtWhaleSigner) + .notifyRewardAmount(parseEther('1'), parseEther('1')), + ).revertedWith('Wrong caller') + }) + }) + + describe('Claim', async () => { + beforeEach(async function () { + await InitializeStakingRewards() + await sdt.approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + await sdFRAX3CRVContract + .connect(deployerSigner) + .approve(stakingRewards.address, parseEther('10')) + }) + + it('Users with stake deposited could claim a reward if available', async function () { + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + }) + + it('Users get reward in proportion to the amount they have deposited', async function () { + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.connect(baseOwner).getReward() + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('2'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + }) + + it('Users cannot claim a reward more than once if no rewards are added after the first claim', async function () { + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + ethers.provider.send('evm_increaseTime', [10]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('3'), + ) + await expect(stakingRewards.connect(baseOwner).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('3'), + ) + }) + + it('Multiple users could claim their reward', async function () { + await sdt + .connect(sdtWhaleSigner) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(sdtWhaleSigner) + ['stake(uint256,uint256)'](parseEther('1'), 224) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [10]) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('3'), + ) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('3.5'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal(parseEther('0')) + await expect(stakingRewards.connect(sdtWhaleSigner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal(parseEther('0.5')) + }) + + it('Users would be able to claim rewards after each reward deposit', async function () { + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [10]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [10]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('5.5'), + ) + }) + + it('Users would be able to claim multiple rewards in one go', async function () { + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [10]) + + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [10]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('7.5'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + }) + + it('Users cannot claim a reward if they have not staked anything', async function () { + await expect( + stakingRewards.connect(sdFrax3CRVWhaleSigner).getReward(), + ).not.emit(stakingRewards, 'RewardPaid') + }) + + it('we should be able to distribute rewards added by someone else', async function () { + await sdFRAX3CRVContract + .connect(sdFrax3CRVWhaleSigner) + .transfer(stakingRewards.address, parseEther('10')) + + await stakingRewards.notifyRewardAmount( + parseEther('11'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('11')) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('18.5'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + }) + + it('Multiple users could claim their reward with multiple rewards being added', async function () { + await sdt.approve(stakingRewards.address, parseEther('99')) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('99'), 223) + + await sdt + .connect(sdtWhaleSigner) + .approve(stakingRewards.address, parseEther('200')) + await stakingRewards + .connect(sdtWhaleSigner) + ['stake(uint256,uint256)'](parseEther('200'), 224) + + await sdFRAX3CRVContract + .connect(deployerSigner) + .approve(stakingRewards.address, parseEther('300')) + await stakingRewards.notifyRewardAmount( + parseEther('300'), + parseEther('300'), + ) + + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('300')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('300'), 225) + + await sdFRAX3CRVContract + .connect(deployerSigner) + .approve(stakingRewards.address, parseEther('300')) + await stakingRewards.notifyRewardAmount( + parseEther('300'), + parseEther('300'), + ) + + ethers.provider.send('evm_increaseTime', [10]) + + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + await expect(stakingRewards.connect(sdtWhaleSigner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + await expect(stakingRewards.connect(userC).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('168.5'), //150 claimed + ) + + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal(parseEther('300.5')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + parseEther('150'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + }) + }) + + describe('Vesting', async () => { + beforeEach(async function () { + await InitializeStakingRewards() + await sdt.approve(stakingRewards.address, parseEther('1')) + await sdFRAX3CRVContract + .connect(deployerSigner) + .approve(stakingRewards.address, parseEther('10')) + }) + + it('Duration of 7 days & withdrawal after one second of reward notification will result in rewards received for one second', async function () { + await stakingRewards.setDuration(7 * 86400) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + + ethers.provider.send('evm_increaseTime', [1]) + await stakingRewards.connect(baseOwner).withdraw(parseEther('1')) + ethers.provider.send('evm_increaseTime', [24 * 7 * 60 * 60]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('168.500001653439153439'), + ) + + await expect(stakingRewards.connect(baseOwner).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0.999998346560846561')) + }) + + it('Staking after reward notification', async function () { + await stakingRewards.setDuration(7 * 86400) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + + ethers.provider.send('evm_increaseTime', [24 * 7 * 60 * 60]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('169.499999999999907200'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0.000001653439246239')) + }) + + it('Staking before reward notification: Duration 7 days', async function () { + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + await stakingRewards.setDuration(7 * 86400) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + + ethers.provider.send('evm_increaseTime', [24 * 7 * 60 * 60]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('170.499999999999814400'), //Increment of 0.999999999999907200 + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0.000000000000092800')) //There is a loss of some amount + }) + + it('Staking before reward notification: Duration 10 days', async function () { + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + await stakingRewards.setDuration(10 * 86400) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + + ethers.provider.send('evm_increaseTime', [24 * 10 * 60 * 60]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('171.499999999999462400'), //Increment of 0.999999999999648000 + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0.000000000000352000')) //There is a loss of some amount + }) + + it('Staking midway during the vesting period: Duration 7 days', async function () { + await stakingRewards.setDuration(7 * 86400) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + ethers.provider.send('evm_increaseTime', [24 * 3.5 * 60 * 60]) + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + ethers.provider.send('evm_increaseTime', [24 * 3.5 * 60 * 60]) + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('171.999999999999416000'), //Increment of 0.499,999,999,999,953,600 + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0.500000000000046400')) //There is a loss of some amount 92800 + }) + + it('Staking after duration expires', async function () { + const initBalance = parseEther('171.999999999999416000') + //Notify reward + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + initBalance, + ) + ethers.provider.send('evm_increaseTime', [10]) + + //Stake after expiration + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + + //Claim reward + await expect(stakingRewards.connect(baseOwner).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + initBalance, //Balance do not change as the staking was done after the notification of rewards + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + }) + + it('1.1.1 Stake before notification & withdrawal during vesting period', async function () { + //Stake + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //User claims the rewards + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('172.999999999999416000'), // Gets 1 reward token + ) + ethers.provider.send('evm_increaseTime', [10]) + + //Users withdraw their stake + await stakingRewards.connect(baseOwner).withdraw(parseEther('1')) + + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('172.999999999999416000'), + ) + }) + + it('1.1.2 Stake before notification & withdrawal during vesting period', async function () { + //Stake + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + ethers.provider.send('evm_increaseTime', [1]) + //User claims the rewards + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('173.999999999999416000'), // Gets 1 reward token + ) + + //Users withdraw their stake + await stakingRewards.connect(baseOwner).withdraw(parseEther('1')) + + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('173.999999999999416000'), + ) + }) + + it('1.1.3 Stake before notification & withdrawal during vesting period', async function () { + //Stake + await stakingRewards + .connect(baseOwner) + ['stake(uint256,uint256)'](parseEther('1'), 223) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + ethers.provider.send('evm_increaseTime', [1]) + + //Users withdraw their stake + await stakingRewards.connect(baseOwner).withdraw(parseEther('1')) + //User claims the rewards + await expect(stakingRewards.connect(baseOwner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect(await sdFRAX3CRVContract.balanceOf(baseOwner.address)).to.equal( + parseEther('174.999999999999416000'), // Gets 1 reward token + ) + }) + + it('1.2.1 Stake before notification & withdrawal during vesting period', async function () { + //Stake + await sdt + .connect(sdtWhaleSigner) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(sdtWhaleSigner) + ['stake(uint256,uint256)'](parseEther('1'), 224) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //User claims the rewards + await expect(stakingRewards.connect(sdtWhaleSigner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal( + parseEther('301.5'), // Gets 1 reward token + ) + + //Users withdraw their stake + await stakingRewards.connect(sdtWhaleSigner).withdraw(parseEther('1')) + + ethers.provider.send('evm_increaseTime', [1]) + + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal(parseEther('301.5')) + }) + + it('1.2.2 Stake before notification & withdrawal during vesting period', async function () { + //Stake + await sdt + .connect(sdtWhaleSigner) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(sdtWhaleSigner) + ['stake(uint256,uint256)'](parseEther('1'), 224) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Users withdraw their stake + await stakingRewards.connect(sdtWhaleSigner).withdraw(parseEther('1')) + + //User claims the rewards + await expect(stakingRewards.connect(sdtWhaleSigner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal( + parseEther('302.5'), // Gets 1 reward token + ) + }) + + it('1.2.3 Stake before notification & withdrawal during vesting period', async function () { + //Stake + await sdt + .connect(sdtWhaleSigner) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(sdtWhaleSigner) + ['stake(uint256,uint256)'](parseEther('1'), 224) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Users withdraw their stake + await stakingRewards.connect(sdtWhaleSigner).withdraw(parseEther('1')) + + ethers.provider.send('evm_increaseTime', [1]) + + //User claims the rewards + await expect(stakingRewards.connect(sdtWhaleSigner).getReward()).emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('0')) + expect( + await sdFRAX3CRVContract.balanceOf(sdtWhaleSigner.getAddress()), + ).to.equal( + parseEther('303.5'), // Gets 1 reward token + ) + }) + + it('1.3.1 No reward as withdraw before notification', async function () { + const userRewarTokenBalance = parseEther('150') + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + //Users withdraw their stake + await stakingRewards.connect(userC).withdraw(parseEther('1')) + + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + }) + + it('1.3.2 No reward as withdraw before notification', async function () { + const userRewarTokenBalance = parseEther('150') + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + //10 sec passes & rewards are notified + ethers.provider.send('evm_increaseTime', [10]) + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + ethers.provider.send('evm_increaseTime', [1]) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + }) + + it('2.1.1 Staking during duration & claim during duration', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + + ethers.provider.send('evm_increaseTime', [1]) + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + }) + + it('2.1.2 Staking during duration & claim after expiry', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + ethers.provider.send('evm_increaseTime', [1]) + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + }) + + it('2.1.3 Staking during duration & claim after expiry & withdrawal', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + ethers.provider.send('evm_increaseTime', [1]) + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + }) + + it('2.2.1 Staking,claim withdraw during duration', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + }) + + it('2.2.2 Staking, withdraw, claim during duration', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + }) + + it('2.2.3 Staking, withdraw during duration & claim after expiry', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + + ethers.provider.send('evm_increaseTime', [1]) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + }) + + it('2.3.1 Stake after expiry', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [1]) + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + }) + + it('2.3.2 Stake after expiry, claim after withdraw', async function () { + const userRewarTokenBalance = parseEther('150') + //Notify + await stakingRewards.notifyRewardAmount( + parseEther('1'), + parseEther('1'), + ) + ethers.provider.send('evm_increaseTime', [1]) + //Stake + await sdt + .connect(userC) + .approve(stakingRewards.address, parseEther('1')) + await stakingRewards + .connect(userC) + ['stake(uint256,uint256)'](parseEther('1'), 225) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, + ) + + //Users withdraw their stake + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('19')) + await stakingRewards.connect(userC).withdraw(parseEther('1')) + expect(await sdt.balanceOf(userC.getAddress())).equal(parseEther('20')) + + //User claims the rewards + await expect(stakingRewards.connect(userC).getReward()).not.emit( + stakingRewards, + 'RewardPaid', + ) + expect( + await sdFRAX3CRVContract.balanceOf(stakingRewards.address), + ).to.equal(parseEther('1')) + expect(await sdFRAX3CRVContract.balanceOf(userC.getAddress())).to.equal( + userRewarTokenBalance, //No rewards received + ) + }) + }) + }) +}) diff --git a/nft/tsconfig.json b/nft/tsconfig.json new file mode 100644 index 0000000..ea7d94d --- /dev/null +++ b/nft/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["./scripts", "./test", "test/strategySbtc.ts"], + "files": ["./hardhat.config.ts"] +} diff --git a/polygon/abis/BaseRewardPool.json b/polygon/abis/BaseRewardPool.json new file mode 100644 index 0000000..ffc42dc --- /dev/null +++ b/polygon/abis/BaseRewardPool.json @@ -0,0 +1,363 @@ +[ + { + "inputs": [ + {"internalType": "uint256", "name": "pid_", "type": "uint256"}, + {"internalType": "address", "name": "stakingToken_", "type": "address"}, + {"internalType": "address", "name": "rewardToken_", "type": "address"}, + {"internalType": "address", "name": "operator_", "type": "address"}, + {"internalType": "address", "name": "rewardManager_", "type": "address"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [ + {"internalType": "address", "name": "_reward", "type": "address"} + ], + "name": "addExtraReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "clearExtraRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "donate", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "duration", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "earned", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "name": "extraRewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "extraRewardsLength", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_account", "type": "address"}, + {"internalType": "bool", "name": "_claimExtras", "type": "bool"} + ], + "name": "getReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "historicalRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastUpdateTime", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "newRewardRatio", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "operator", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "periodFinish", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pid", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_rewards", "type": "uint256"} + ], + "name": "queueNewRewards", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "queuedRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardManager", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "rewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "stake", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakeAll", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_for", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "stakeFor", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakingToken", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "userRewardPerTokenPaid", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "bool", "name": "claim", "type": "bool"} + ], + "name": "withdraw", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool", "name": "claim", "type": "bool"}], + "name": "withdrawAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool", "name": "claim", "type": "bool"}], + "name": "withdrawAllAndUnwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "bool", "name": "claim", "type": "bool"} + ], + "name": "withdrawAndUnwrap", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/polygon/abis/Controller.json b/polygon/abis/Controller.json new file mode 100644 index 0000000..5241e1d --- /dev/null +++ b/polygon/abis/Controller.json @@ -0,0 +1,320 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "approveStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "approvedStrategies", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "converters", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "getExpectedReturn", + "outputs": [ + {"internalType": "uint256", "name": "expected", "type": "uint256"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "inCaseStrategyTokenGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "inCaseTokensGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onesplit", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "revokeStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_input", "type": "address"}, + {"internalType": "address", "name": "_output", "type": "address"}, + {"internalType": "address", "name": "_converter", "type": "address"} + ], + "name": "setConverter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_onesplit", "type": "address"} + ], + "name": "setOneSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "name": "setRewards", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_split", "type": "uint256"} + ], + "name": "setSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategist", "type": "address"} + ], + "name": "setStrategist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "setStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_vault", "type": "address"} + ], + "name": "setVault", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "split", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "strategies", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "strategist", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "vaults", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "yearn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/polygon/abis/ERC20.json b/polygon/abis/ERC20.json new file mode 100644 index 0000000..f43b285 --- /dev/null +++ b/polygon/abis/ERC20.json @@ -0,0 +1,159 @@ +[ + { + "name": "Transfer", + "inputs": [ + {"type": "address", "name": "_from", "indexed": true}, + {"type": "address", "name": "_to", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Approval", + "inputs": [ + {"type": "address", "name": "_owner", "indexed": true}, + {"type": "address", "name": "_spender", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"}, + {"type": "uint256", "name": "_decimals"}, + {"type": "uint256", "name": "_supply"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "set_minter", + "outputs": [], + "inputs": [{"type": "address", "name": "_minter"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 36247 + }, + { + "name": "set_name", + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 178069 + }, + { + "name": "totalSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1211 + }, + { + "name": "allowance", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "address", "name": "_owner"}, + {"type": "address", "name": "_spender"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1549 + }, + { + "name": "transfer", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 74832 + }, + { + "name": "transferFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 111983 + }, + { + "name": "approve", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_spender"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 39078 + }, + { + "name": "mint", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75808 + }, + { + "name": "burnFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75826 + }, + { + "name": "name", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 7823 + }, + { + "name": "symbol", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 6876 + }, + { + "name": "decimals", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1481 + }, + { + "name": "balanceOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1665 + } +] diff --git a/polygon/abis/YVault.json b/polygon/abis/YVault.json new file mode 100644 index 0000000..6ba3c86 --- /dev/null +++ b/polygon/abis/YVault.json @@ -0,0 +1,347 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "available", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "balance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "controller", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"} + ], + "name": "decreaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "deposit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "depositAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPricePerFullShare", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "reserve", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "harvest", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "addedValue", "type": "uint256"} + ], + "name": "increaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "min", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "name": "setController", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{"internalType": "uint256", "name": "_min", "type": "uint256"}], + "name": "setMin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_shares", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/polygon/contracts/StrategyAm3Crv.sol b/polygon/contracts/StrategyAm3Crv.sol new file mode 100644 index 0000000..39f1cc0 --- /dev/null +++ b/polygon/contracts/StrategyAm3Crv.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface ICurveFi { + function add_liquidity( + uint256[3] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); +} + +interface Gauge { + function deposit(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function withdraw(uint256) external; + + function claim_rewards() external; +} + +contract StrategyAm3Crv { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = + address(0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171); + + address public constant paraswap = + address(0x90249ed4d69D70E709fFCd8beE2c5A566f65dADE); + + address public constant wmatic = + address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + + address public constant crv = + address(0x172370d5Cd63279eFa6d502DAB29171933a610AF); + + address public constant dai = + address(0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063); + + address public constant pool = + address(0x445FE580eF8d70FF569aB36e80c647af338db351); + + address public constant gauge = + address(0xe381C25de995d62b453aF8B931aAc84fcCaa7A62); + + address public paraswapProxy = + address(0xCD52384e2A96F6E91e4e420de2F9a8C0f1FFB449); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return 'StrategyAm3Crv'; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setParaswapProxy(address _paraswapProxy) external onlyGovernance { + paraswapProxy = _paraswapProxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + Gauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(dai != address(_asset), 'dai'); + require(wmatic != address(_asset), 'wmatic'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + Gauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), '!slippage'); + } + + function harvest(bytes memory swapDataWmatic, bytes memory swapDataCrv) + public + { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + + Gauge(gauge).claim_rewards(); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + IERC20(crv).approve(paraswapProxy, _crv); + (bool success, ) = paraswap.call(swapDataCrv); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + + uint256 _wmatic = IERC20(wmatic).balanceOf(address(this)); + if (_wmatic > 0) { + IERC20(wmatic).approve(paraswapProxy, _wmatic); + (bool success, ) = paraswap.call(swapDataWmatic); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).approve(pool, _dai); + ICurveFi(pool).add_liquidity([_dai, 0, 0], 0, true); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return Gauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/polygon/contracts/StrategyBtcCurve.sol b/polygon/contracts/StrategyBtcCurve.sol new file mode 100644 index 0000000..6932ff8 --- /dev/null +++ b/polygon/contracts/StrategyBtcCurve.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface ICurveFi { + function add_liquidity( + uint256[2] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); +} + +interface Gauge { + function deposit(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function withdraw(uint256) external; + + function claim_rewards() external; +} + +contract StrategyBtcCurve { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = + address(0xf8a57c1d3b9629b77b6726a042ca48990A84Fb49); + + address public paraswap = + address(0x90249ed4d69D70E709fFCd8beE2c5A566f65dADE); + + address public constant wmatic = + address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + + address public constant crv = + address(0x172370d5Cd63279eFa6d502DAB29171933a610AF); + + address public constant wbtc = + address(0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6); + + address public constant pool = + address(0xC2d95EEF97Ec6C17551d45e77B590dc1F9117C67); + + address public gauge = + address(0x19793B454D3AfC7b454F206Ffe95aDE26cA6912c); + + address public paraswapProxy = + address(0xCD52384e2A96F6E91e4e420de2F9a8C0f1FFB449); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return 'StrategyBtcCurve'; + } + + function setGauge(address _gauge) external onlyGovernance { + gauge = _gauge; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setParaswapProxy(address _paraswapProxy) external onlyGovernance { + paraswapProxy = _paraswapProxy; + } + + function setParaswap(address _paraswap) external onlyGovernance { + paraswap = _paraswap; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + Gauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(wbtc != address(_asset), 'wbtc'); + require(wmatic != address(_asset), 'wmatic'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + Gauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), '!slippage'); + } + + function harvest(bytes memory swapDataWmatic, bytes memory swapDataCrv) + public + { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + + Gauge(gauge).claim_rewards(); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + IERC20(crv).approve(paraswapProxy, _crv); + (bool success, ) = paraswap.call(swapDataCrv); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + + uint256 _wmatic = IERC20(wmatic).balanceOf(address(this)); + if (_wmatic > 0) { + IERC20(wmatic).approve(paraswapProxy, _wmatic); + (bool success, ) = paraswap.call(swapDataWmatic); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + uint256 _wbtc = IERC20(wbtc).balanceOf(address(this)); + if (_wbtc > 0) { + IERC20(wbtc).approve(pool, _wbtc); + ICurveFi(pool).add_liquidity([_wbtc, 0], 0, true); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return Gauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/polygon/contracts/Vault.sol b/polygon/contracts/Vault.sol new file mode 100644 index 0000000..2aabb2e --- /dev/null +++ b/polygon/contracts/Vault.sol @@ -0,0 +1,144 @@ +pragma solidity ^0.5.17; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol'; +import '@openzeppelin/contracts/ownership/Ownable.sol'; + + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +contract Vault is ERC20, ERC20Detailed { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + address public governance; + address public controller; + + constructor( + address _token, + address _controller, + address _governance + ) + public + ERC20Detailed( + string(abi.encodePacked('Stake DAO ', ERC20Detailed(_token).name())), + string(abi.encodePacked('sd', ERC20Detailed(_token).symbol())), + ERC20Detailed(_token).decimals() + ) + { + token = IERC20(_token); + governance = msg.sender; + controller = _controller; + governance = _governance; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, '!governance'); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, '!governance'); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, '!governance'); + controller = _controller; + } + + // Custom logic in here for how much the vault allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, '!controller'); + require(reserve != address(token), 'token'); + IERC20(reserve).safeTransfer(controller, amount); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + token.safeTransfer(msg.sender, r); + } + + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? 1e18 : balance().mul(1e18).div(totalSupply()); + } +} diff --git a/polygon/hardhat.config.ts b/polygon/hardhat.config.ts new file mode 100644 index 0000000..40936ee --- /dev/null +++ b/polygon/hardhat.config.ts @@ -0,0 +1,46 @@ +import {HardhatUserConfig} from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-waffle'; +import '@nomiclabs/hardhat-etherscan'; +import 'hardhat-tracer'; + +import './tasks/deploy-usd'; +import './tasks/deploy-btc'; +import './tasks/deploy-eur'; +import './tasks/deploy-am-3crv'; + +require('dotenv').config(); + +const DEPLOYER = process.env.DEPLOYER; + +export default { + defaultNetwork: 'hardhat', + networks: { + hardhat: { + forking: { + //url: process.env.ALCHEMY_MAINNET + url: '' + } + }, + mainnet: { + url: process.env.ALCHEMY_MAINNET, + accounts: [`0x${DEPLOYER}`] + }, + matic: { + url: ``, + accounts: [`0x${DEPLOYER}`], + gasPrice: 9000000000 + } + }, + solidity: { + compilers: [ + {version: '0.8.0'}, + {version: '0.7.4'}, + {version: '0.6.12'}, + {version: '0.5.17'} + ] + }, + etherscan: { + apiKey: process.env.ETHERSCAN_KEY + } +} as HardhatUserConfig; diff --git a/polygon/package.json b/polygon/package.json new file mode 100644 index 0000000..dcc722a --- /dev/null +++ b/polygon/package.json @@ -0,0 +1,35 @@ +{ + "name": "convex-test", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@nomiclabs/hardhat-etherscan": "^2.1.2", + "@nomiclabs/hardhat-waffle": "^2.0.0", + "@types/chai": "^4.2.17", + "@types/mocha": "^8.2.2", + "@types/node": "^15.0.1", + "chai": "^4.3.4", + "ethereum-waffle": "^3.0.0", + "ethers": "^5.0.0", + "hardhat": "^2.2.1", + "hardhat-abi-exporter": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.10", + "ts-node": "^9.1.1", + "typescript": "^4.2.4" + }, + "dependencies": { + "@0xsequence/erc-1155": "^3.0.4", + "@chainlink/contracts": "^0.1.7", + "@ethersproject/units": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@openzeppelin/contracts": "2.5.1", + "@uniswap/v3-periphery": "^1.1.0", + "dotenv": "^8.2.0", + "hardhat-tracer": "1.0.0-alpha.6", + "prettier": "^2.3.0" + }, + "scripts": { + "prettier": "prettier --write 'contracts/**/*.sol'" + } +} diff --git a/polygon/readme.md b/polygon/readme.md new file mode 100644 index 0000000..9e92ae2 --- /dev/null +++ b/polygon/readme.md @@ -0,0 +1,3 @@ +## Run test + +`npx hardhat test test/strategy.ts` diff --git a/polygon/test/strategyAm3crv.ts b/polygon/test/strategyAm3crv.ts new file mode 100644 index 0000000..1cc9a02 --- /dev/null +++ b/polygon/test/strategyAm3crv.ts @@ -0,0 +1,85 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const VAULT = '0x7d60F21072b585351dFd5E8b17109458D97ec120'; +const CONTROLLER = '0x91aE00aaC6eE0D7853C8F92710B641F68Cd945Df'; +const GOVERNANCE = '0xb36a0671B3D49587236d7833B01E79798175875f'; +const OLD_STRATEGY = '0x552DAd974da30D67f25BE444991E22CbaE357851'; + +const CRV = '0x172370d5Cd63279eFa6d502DAB29171933a610AF'; +const WANT = '0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171'; // am3CRV + +describe('StrategyAm3Crv', function () { + let vault: Contract; + let want: Contract; + let crv: Contract; + let controller: Contract; + let strategy: Contract; + let oldStrategy: Contract; + let governanceSigner: JsonRpcSigner; + + let initialBalance: BigNumber; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + const Strategy = await ethers.getContractFactory('StrategyAm3Crv'); + + vault = await ethers.getContractAt(YVault, VAULT); + controller = await ethers.getContractAt(Controller, CONTROLLER); + want = await ethers.getContractAt(ERC20, WANT); + crv = await ethers.getContractAt(ERC20, CRV); + oldStrategy = await ethers.getContractAt('StrategyAm3Crv', OLD_STRATEGY); + + strategy = await Strategy.deploy(CONTROLLER); + + governanceSigner = await ethers.provider.getSigner(GOVERNANCE); + + initialBalance = await vault.balance(); + + await ( + await controller + .connect(governanceSigner) + .approveStrategy(WANT, strategy.address) + ).wait(); + + await ( + await controller + .connect(governanceSigner) + .setStrategy(WANT, strategy.address) + ).wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(initialBalance)).to.be.true; + }); + + it('Should send funds to strategy', async function () { + await (await vault.earn()).wait(); + await (await vault.earn()).wait(); + await (await vault.earn()).wait(); + + const strategyPoolBalance = await strategy.balanceOfPool(); + expect(strategyPoolBalance.gt(0)).to.be.true; + }); +}); diff --git a/polygon/test/strategySbtc.ts b/polygon/test/strategySbtc.ts new file mode 100644 index 0000000..13e9857 --- /dev/null +++ b/polygon/test/strategySbtc.ts @@ -0,0 +1,243 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const VAULT = '0x24129B935AfF071c4f0554882C0D9573F4975fEd'; +const WANT = '0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3'; +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x282742940eE0b7ed028Bb48052Bb4922282234dA'; + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexSbtc', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Strategy = await ethers.getContractFactory('StrategySbtcConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + strategy = await Strategy.deploy(CONTROLLER, PROXY); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await ethers.getContractAt(YVault, VAULT); + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaultSbtcBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(vaultSbtcBalance)).to.be.true; + + console.log({vaultBalance, vaultSbtcBalance}); + }); + + it('should deposit 95% of vault balance in convex sBTC pool', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(VAULT); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfterEarn.gt(balanceBefore)).to.be.true; + expect(expectedWithdrawFromStrategy.gte(balanceDifference)).to.be.true; + expect(expectedUserBalanceWantAfter.eq(userWantBalanceAfter)).to.be.true; + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(VAULT); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(VAULT); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/polygon/tsconfig.json b/polygon/tsconfig.json new file mode 100644 index 0000000..ea7d94d --- /dev/null +++ b/polygon/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["./scripts", "./test", "test/strategySbtc.ts"], + "files": ["./hardhat.config.ts"] +} diff --git a/protocol b/protocol new file mode 160000 index 0000000..060a47f --- /dev/null +++ b/protocol @@ -0,0 +1 @@ +Subproject commit 060a47f0c7c530bbdc0363dd2d994efb2e932873