diff --git a/Kovan-contracts-address.txt b/Kovan-contracts-address.txt index 06091ea..6f3e9ee 100644 --- a/Kovan-contracts-address.txt +++ b/Kovan-contracts-address.txt @@ -1,6 +1,12 @@ -Controller : +old Controller : 0xafF55941113bd838abEF4Dd9d4A0b7E5a010470e +new Controller (with LM) : +Proxy : +0x957EEbF87f1adAD8F9862412f76247F305fe8F4d +Implementation : +0xFcA9E6b4a2b77cC75dC3e40cEbe8141e5F726800 + Interest Calculator : 0x0A06DfeCBABECAa6887B0D8e680A45E9e86a9838 diff --git a/abi/PaladinControllerInterface.json b/abi/PaladinControllerInterface.json index 9b00eb1..376358f 100644 --- a/abi/PaladinControllerInterface.json +++ b/abi/PaladinControllerInterface.json @@ -1,437 +1,1122 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "palToken", - "type": "address" - } - ], - "name": "NewPalPool", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "palToken", - "type": "address" - } - ], - "name": "RemovePalPool", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palToken", - "type": "address" - }, - { - "internalType": "address", - "name": "palPool", - "type": "address" - } - ], - "name": "addNewPool", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "borrowPossible", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "address", - "name": "borrower", - "type": "address" - }, - { - "internalType": "address", - "name": "delegatee", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "feesAmount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "loanAddress", - "type": "address" - } - ], - "name": "borrowVerify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "address", - "name": "borrower", - "type": "address" - }, - { - "internalType": "address", - "name": "loanAddress", - "type": "address" - } - ], - "name": "closeBorrowVerify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "address", - "name": "dest", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "depositVerify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "address", - "name": "loanAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "newFeesAmount", - "type": "uint256" - } - ], - "name": "expandBorrowVerify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPalPools", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPalTokens", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "pool", - "type": "address" - } - ], - "name": "isPalPool", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "address", - "name": "killer", - "type": "address" - }, - { - "internalType": "address", - "name": "loanAddress", - "type": "address" - } - ], - "name": "killBorrowVerify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_palPool", - "type": "address" - } - ], - "name": "removePool", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "palTokens", - "type": "address[]" - }, - { - "internalType": "address[]", - "name": "palPools", - "type": "address[]" - } - ], - "name": "setInitialPools", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_newController", - "type": "address" - } - ], - "name": "setPoolsNewController", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_pool", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "withdrawFromPool", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "withdrawPossible", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "palPool", - "type": "address" - }, - { - "internalType": "address", - "name": "dest", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "withdrawVerify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - } - ] \ No newline at end of file + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimRewards", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "palToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdmin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "palToken", + "type": "address" + } + ], + "name": "NewPalPool", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newSupplySpeed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBorrowRatio", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "autoBorrowReward", + "type": "bool" + } + ], + "name": "PoolRewardsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "palToken", + "type": "address" + } + ], + "name": "RemovePalPool", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "palToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "accruedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_palToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_palPool", + "type": "address" + } + ], + "name": "addNewPool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "autoBorrowRewards", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ControllerProxy", + "name": "proxy", + "type": "address" + } + ], + "name": "becomeImplementation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "borrowPossible", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "borrowRatios", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feesAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "loanAddress", + "type": "address" + } + ], + "name": "borrowVerify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "loanAddress", + "type": "address" + } + ], + "name": "claimLoanRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "claimable", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "loanAddress", + "type": "address" + } + ], + "name": "claimableLoanRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "loanAddress", + "type": "address" + } + ], + "name": "closeBorrowVerify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "dest", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "depositVerify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "estimateClaimable", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "loanAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newFeesAmount", + "type": "uint256" + } + ], + "name": "expandBorrowVerify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getPalPools", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPalTokens", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialRewardsIndex", + "outputs": [ + { + "internalType": "uint224", + "name": "", + "type": "uint224" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isLoanRewardClaimed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_pool", + "type": "address" + } + ], + "name": "isPalPool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "killer", + "type": "address" + }, + { + "internalType": "address", + "name": "loanAddress", + "type": "address" + } + ], + "name": "killBorrowVerify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "loansBorrowRatios", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "palPools", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "palTokenToPalPool", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "palTokens", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + } + ], + "name": "removePool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_palTokens", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_palPools", + "type": "address[]" + } + ], + "name": "setInitialPools", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "_newAdmin", + "type": "address" + } + ], + "name": "setNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newController", + "type": "address" + } + ], + "name": "setPoolsNewController", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "supplierDeposits", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "supplierRewardIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "supplyRewardState", + "outputs": [ + { + "internalType": "uint224", + "name": "index", + "type": "uint224" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "supplySpeeds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "totalSupplierDeposits", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupplyRewardSpeed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newSupplySpeed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newBorrowRatio", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "autoBorrowReward", + "type": "bool" + } + ], + "name": "updatePoolRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newRewardTokenAddress", + "type": "address" + } + ], + "name": "updateRewardToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "updateUserRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "withdrawFromPool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawPossible", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "palPool", + "type": "address" + }, + { + "internalType": "address", + "name": "dest", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawVerify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/contracts/ControllerProxy.sol b/contracts/ControllerProxy.sol new file mode 100644 index 0000000..77ca646 --- /dev/null +++ b/contracts/ControllerProxy.sol @@ -0,0 +1,77 @@ +//██████╗ █████╗ ██╗ █████╗ ██████╗ ██╗███╗ ██╗ +//██╔══██╗██╔══██╗██║ ██╔══██╗██╔══██╗██║████╗ ██║ +//██████╔╝███████║██║ ███████║██║ ██║██║██╔██╗ ██║ +//██╔═══╝ ██╔══██║██║ ██╔══██║██║ ██║██║██║╚██╗██║ +//██║ ██║ ██║███████╗██║ ██║██████╔╝██║██║ ╚████║ +//╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═══╝ + + +pragma solidity ^0.7.6; +//SPDX-License-Identifier: MIT + +import "./utils/Errors.sol"; +import "./ControllerStorage.sol"; + +/** @title Paladin Controller contract */ +/// @author Paladin +contract ControllerProxy is ControllerStorage { + + event NewPendingImplementation(address oldPendingImplementation, address newPendingImplementation); + + event NewImplementation(address oldImplementation, address newImplementation); + + constructor(){ + admin = msg.sender; + } + + /** + * @dev Proposes the address of a new Implementation (the new Controller contract) + */ + function proposeImplementation(address newPendingImplementation) public adminOnly { + + address oldPendingImplementation = pendingImplementation; + + pendingImplementation = newPendingImplementation; + + emit NewPendingImplementation(oldPendingImplementation, newPendingImplementation); + } + + /** + * @dev Accepts the Pending Implementation as new Current Implementation + * Only callable by the Pending Implementation contract + */ + function acceptImplementation() public returns(bool) { + require(msg.sender == pendingImplementation || pendingImplementation == address(0), Errors.CALLER_NOT_IMPLEMENTATION); + + address oldImplementation = currentImplementation; + address oldPendingImplementation = pendingImplementation; + + currentImplementation = pendingImplementation; + pendingImplementation = address(0); + + emit NewImplementation(oldImplementation, currentImplementation); + emit NewPendingImplementation(oldPendingImplementation, pendingImplementation); + + return true; + } + + /** + * @dev Delegates execution to an implementation contract. + * It returns to the external caller whatever the implementation returns + * or forwards reverts. + */ + fallback() external payable { + // delegate all other functions to current implementation + (bool success, ) = currentImplementation.delegatecall(msg.data); + + assembly { + let free_mem_ptr := mload(0x40) + returndatacopy(free_mem_ptr, 0, returndatasize()) + + switch success + case 0 { revert(free_mem_ptr, returndatasize()) } + default { return(free_mem_ptr, returndatasize()) } + } + } + +} \ No newline at end of file diff --git a/contracts/ControllerStorage.sol b/contracts/ControllerStorage.sol new file mode 100644 index 0000000..b91c930 --- /dev/null +++ b/contracts/ControllerStorage.sol @@ -0,0 +1,87 @@ +//██████╗ █████╗ ██╗ █████╗ ██████╗ ██╗███╗ ██╗ +//██╔══██╗██╔══██╗██║ ██╔══██╗██╔══██╗██║████╗ ██║ +//██████╔╝███████║██║ ███████║██║ ██║██║██╔██╗ ██║ +//██╔═══╝ ██╔══██║██║ ██╔══██║██║ ██║██║██║╚██╗██║ +//██║ ██║ ██║███████╗██║ ██║██████╔╝██║██║ ╚████║ +//╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═══╝ + + +pragma solidity ^0.7.6; +//SPDX-License-Identifier: MIT + +import "./utils/Admin.sol"; + +/** @title Paladin Controller contract */ +/// @author Paladin +contract ControllerStorage is Admin { + + /** @notice Layout for the Proxy contract */ + address public currentImplementation; + address public pendingImplementation; + + /** @notice List of current active palToken Pools */ + address[] public palTokens; + address[] public palPools; + mapping(address => address) public palTokenToPalPool; + + bool internal initialized; + + /** @notice Struct with current SupplyIndex for a Pool, and the block of the last update */ + struct PoolRewardsState { + uint224 index; + uint32 blockNumber; + } + + /** @notice Initial index for Rewards */ + uint224 public constant initialRewardsIndex = 1e36; + + address public rewardTokenAddress; // PAL token address to put here + + /** @notice State of the Rewards for each Pool */ + mapping(address => PoolRewardsState) public supplyRewardState; + + /** @notice Amount of reward tokens to distribute each block */ + mapping(address => uint) public supplySpeeds; + + /** @notice Last reward index for each Pool for each user */ + /** PalPool => User => Index */ + mapping(address => mapping(address => uint)) public supplierRewardIndex; + + /** @notice Deposited amounts by user for each palToken (indexed by corresponding PalPool address) */ + /** PalPool => User => Amount */ + mapping(address => mapping(address => uint)) public supplierDeposits; + + /** @notice Total amount of each palToken deposited (indexed by corresponding PalPool address) */ + /** PalPool => Total Amount */ + mapping(address => uint) public totalSupplierDeposits; + + /** @notice Ratio to distribute Borrow Rewards */ + mapping(address => uint) public borrowRatios; // scaled 1e18 + + /** @notice Ratio for each PalLoan (set at PalLoan creation) */ + mapping(address => uint) public loansBorrowRatios; // scaled 1e18 + + /** @notice Amount of reward Tokens accrued by the user, and claimable */ + mapping(address => uint) public accruedRewards; + + /** @notice Is Auto Borrow Rewards is activated for the PalPool */ + mapping(address => bool) public autoBorrowRewards; + + /** @notice Was PalLoan Borrow Rewards distributed & claimed */ + mapping(address => bool) public isLoanRewardClaimed; + + /** @notice Block at which Borrow Rewards Ratio where set for the PalPool (if Ratio is put back to 0, this block number is set back to 0 too) */ + /** So PalLoan started when no Borrow Rewards where set do not receive rewards */ + /** PalPool => Block Number */ + mapping(address => uint) public borrowRewardsStartBlock; + + /* + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!! ALWAYS PUT NEW STORAGE AT THE BOTTOM !!!!!!!!!!!!!!!!!! + !!!!!!!!! WE DON'T WANT COLLISION WHEN SWITCHING IMPLEMENTATIONS !!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + */ + + +} \ No newline at end of file diff --git a/contracts/IPaladinController.sol b/contracts/IPaladinController.sol index b46cf9a..c81edfe 100644 --- a/contracts/IPaladinController.sol +++ b/contracts/IPaladinController.sol @@ -9,6 +9,8 @@ pragma solidity ^0.7.6; //SPDX-License-Identifier: MIT +import "./ControllerProxy.sol"; + /** @title Paladin Controller Interface */ /// @author Paladin interface IPaladinController { @@ -20,6 +22,13 @@ interface IPaladinController { /** @notice Event emitted when a token & pool are removed from the list */ event RemovePalPool(address palPool, address palToken); + event Deposit(address indexed user, address palToken, uint amount); + event Withdraw(address indexed user, address palToken, uint amount); + + event ClaimRewards(address indexed user, uint amount); + + event PoolRewardsUpdated(address palPool, uint newSupplySpeed, uint newBorrowRatio, bool autoBorrowReward); + //Functions function isPalPool(address pool) external view returns(bool); @@ -27,20 +36,36 @@ interface IPaladinController { function getPalPools() external view returns(address[] memory); function setInitialPools(address[] memory palTokens, address[] memory palPools) external returns(bool); function addNewPool(address palToken, address palPool) external returns(bool); - function removePool(address _palPool) external returns(bool); + function removePool(address palPool) external returns(bool); function withdrawPossible(address palPool, uint amount) external view returns(bool); function borrowPossible(address palPool, uint amount) external view returns(bool); - function depositVerify(address palPool, address dest, uint amount) external view returns(bool); - function withdrawVerify(address palPool, address dest, uint amount) external view returns(bool); - function borrowVerify(address palPool, address borrower, address delegatee, uint amount, uint feesAmount, address loanAddress) external view returns(bool); - function expandBorrowVerify(address palPool, address loanAddress, uint newFeesAmount) external view returns(bool); - function closeBorrowVerify(address palPool, address borrower, address loanAddress) external view returns(bool); - function killBorrowVerify(address palPool, address killer, address loanAddress) external view returns(bool); + function depositVerify(address palPool, address dest, uint amount) external returns(bool); + function withdrawVerify(address palPool, address dest, uint amount) external returns(bool); + function borrowVerify(address palPool, address borrower, address delegatee, uint amount, uint feesAmount, address loanAddress) external returns(bool); + function expandBorrowVerify(address palPool, address loanAddress, uint newFeesAmount) external returns(bool); + function closeBorrowVerify(address palPool, address borrower, address loanAddress) external returns(bool); + function killBorrowVerify(address palPool, address killer, address loanAddress) external returns(bool); + + // PalToken Deposit/Withdraw functions + function deposit(address palToken, uint amount) external returns(bool); + function withdraw(address palToken, uint amount) external returns(bool); + + // Rewards functions + function totalSupplyRewardSpeed() external view returns(uint); + function claimable(address user) external view returns(uint); + function estimateClaimable(address user) external view returns(uint); + function updateUserRewards(address user) external; + function claim(address user) external; + function claimableLoanRewards(address palPool, address loanAddress) external view returns(uint); + function claimLoanRewards(address palPool, address loanAddress) external; //Admin functions - function setPoolsNewController(address _newController) external returns(bool); - function withdrawFromPool(address _pool, uint _amount, address _recipient) external returns(bool); + function becomeImplementation(ControllerProxy proxy) external; + function updateRewardToken(address newRewardTokenAddress) external; + function updatePoolRewards(address palPool, uint newSupplyspeed, uint newBorrowRatio, bool autoBorrowReward) external; + function setPoolsNewController(address newController) external returns(bool); + function withdrawFromPool(address pool, uint amount, address recipient) external returns(bool); } \ No newline at end of file diff --git a/contracts/PaladinController.sol b/contracts/PaladinController.sol index 5899347..7af19d3 100644 --- a/contracts/PaladinController.sol +++ b/contracts/PaladinController.sol @@ -11,29 +11,29 @@ pragma solidity ^0.7.6; import "./utils/SafeMath.sol"; import "./IPaladinController.sol"; +import "./ControllerStorage.sol"; +import "./ControllerProxy.sol"; import "./PalPool.sol"; import "./IPalPool.sol"; -import "./IPalLoan.sol"; +import "./IPalToken.sol"; import "./utils/IERC20.sol"; -import "./utils/Admin.sol"; +import "./utils/SafeERC20.sol"; +import "./utils/Errors.sol"; /** @title Paladin Controller contract */ /// @author Paladin -contract PaladinController is IPaladinController, Admin { +contract PaladinController is IPaladinController, ControllerStorage { using SafeMath for uint; - - - /** @notice List of current active palToken Pools */ - address[] public palTokens; - address[] public palPools; - - bool private initialized = false; + using SafeERC20 for IERC20; constructor(){ admin = msg.sender; } - //Check if an address is a valid palPool + /** + * @notice Check if an address is a valid palPool + * @return bool : result + */ function isPalPool(address _pool) public view override returns(bool){ //Check if the given address is in the palPools list address[] memory _pools = palPools; @@ -70,11 +70,24 @@ contract PaladinController is IPaladinController, Admin { * @return bool : Success */ function setInitialPools(address[] memory _palTokens, address[] memory _palPools) external override adminOnly returns(bool){ - require(!initialized, "Lists already set"); - require(_palTokens.length == _palPools.length, "List sizes not equal"); + require(!initialized, Errors.POOL_LIST_ALREADY_SET); + require(_palTokens.length == _palPools.length, Errors.LIST_SIZES_NOT_EQUAL); palPools = _palPools; palTokens = _palTokens; initialized = true; + + for(uint i = 0; i < _palPools.length; i++){ + //Update the Reward State for the new Pool + PoolRewardsState storage supplyState = supplyRewardState[_palPools[i]]; + if(supplyState.index == 0){ + supplyState.index = initialRewardsIndex; + } + supplyState.blockNumber = safe32(block.number); + + //Link PalToken with PalPool + palTokenToPalPool[_palTokens[i]] = _palPools[i]; + } + return true; } @@ -86,11 +99,24 @@ contract PaladinController is IPaladinController, Admin { */ function addNewPool(address _palToken, address _palPool) external override adminOnly returns(bool){ //Add a new address to the palToken & palPool list - require(!isPalPool(_palPool), "Already added"); + require(!isPalPool(_palPool), Errors.POOL_ALREADY_LISTED); palTokens.push(_palToken); palPools.push(_palPool); + palTokenToPalPool[_palToken] = _palPool; + + //Update the Reward State for the new Pool + PoolRewardsState storage supplyState = supplyRewardState[_palPool]; + if(supplyState.index == 0){ + supplyState.index = initialRewardsIndex; + } + supplyState.blockNumber = safe32(block.number); + //The other Reward values should already be set at 0 : + //BorrowRatio : 0 + //SupplySpeed : 0 + //If not set as 0, we want to use last values (or change them with the adequate function beforehand) + emit NewPalPool(_palPool, _palToken); return true; @@ -99,20 +125,22 @@ contract PaladinController is IPaladinController, Admin { /** * @notice Remove a PalPool from the list (& the related PalToken) - * @param _palPool address of the PalPool contract to remove + * @param palPool address of the PalPool contract to remove * @return bool : Success */ - function removePool(address _palPool) external override adminOnly returns(bool){ + function removePool(address palPool) external override adminOnly returns(bool){ //Remove a palToken & palPool from the list - require(isPalPool(_palPool), "Not listed"); + require(isPalPool(palPool), Errors.POOL_NOT_LISTED); address[] memory _pools = palPools; uint lastIndex = (_pools.length).sub(1); for(uint i = 0; i < _pools.length; i++){ - if(_pools[i] == _palPool){ + if(_pools[i] == palPool){ //get the address of the PalToken for the Event - address _palToken = _pools[i]; + address _palToken = palTokens[i]; + + delete palTokenToPalPool[_palToken]; //Replace the address to remove with the last one of the array palPools[i] = palPools[lastIndex]; @@ -122,7 +150,7 @@ contract PaladinController is IPaladinController, Admin { palPools.pop(); palTokens.pop(); - emit RemovePalPool(_palPool, _palToken); + emit RemovePalPool(palPool, _palToken); return true; } @@ -165,12 +193,11 @@ contract PaladinController is IPaladinController, Admin { * @return bool : Verification Success */ function depositVerify(address palPool, address dest, uint amount) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); - + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); + palPool; dest; - amount; - + //Check the amount sent isn't null return amount > 0; } @@ -184,11 +211,10 @@ contract PaladinController is IPaladinController, Admin { * @return bool : Verification Success */ function withdrawVerify(address palPool, address dest, uint amount) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); - + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); + palPool; dest; - amount; //Check the amount sent isn't null return amount > 0; @@ -204,15 +230,18 @@ contract PaladinController is IPaladinController, Admin { * @param loanAddress address of the new deployed PalLoan * @return bool : Verification Success */ - function borrowVerify(address palPool, address borrower, address delegatee, uint amount, uint feesAmount, address loanAddress) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + function borrowVerify(address palPool, address borrower, address delegatee, uint amount, uint feesAmount, address loanAddress) external override returns(bool){ + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); - palPool; borrower; delegatee; amount; feesAmount; - loanAddress; + + // Set the borrowRatio for this new Loan + if(autoBorrowRewards[palPool]) setLoanBorrowRewards(palPool, loanAddress); //Because some PalPool call this method as view + //So we need to know if the call can update storage or not + //If not, will need to use the Manual Borrow Reward claim system //no method yet return true; @@ -224,12 +253,15 @@ contract PaladinController is IPaladinController, Admin { * @param newFeesAmount new amount of fees in the PalLoan * @return bool : Verification Success */ - function expandBorrowVerify(address palPool, address loanAddress, uint newFeesAmount) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + function expandBorrowVerify(address palPool, address loanAddress, uint newFeesAmount) external override returns(bool){ + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); - palPool; - loanAddress; newFeesAmount; + + // In case the Loan is expanded, the new ratio is used (in case the ratio changed) + if(autoBorrowRewards[palPool]) setLoanBorrowRewards(palPool, loanAddress); //Because some PalPool call this method as view + //So we need to know if the call can update storage or not + //If not, will need to use the Manual Borrow Reward claim system //no method yet return true; @@ -243,12 +275,15 @@ contract PaladinController is IPaladinController, Admin { * @param loanAddress address of the PalLoan contract to close * @return bool : Verification Success */ - function closeBorrowVerify(address palPool, address borrower, address loanAddress) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + function closeBorrowVerify(address palPool, address borrower, address loanAddress) external override returns(bool){ + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); - palPool; borrower; - loanAddress; + + //Accrue Rewards to the Loan's owner + if(autoBorrowRewards[palPool]) accrueBorrowRewards(palPool, loanAddress); //Because some PalPool call this method as view + //So we need to know if the call can update storage or not + //If not, will need to use the Manual Borrow Reward claim system //no method yet return true; @@ -262,37 +297,419 @@ contract PaladinController is IPaladinController, Admin { * @param loanAddress address of the PalLoan contract to kill * @return bool : Verification Success */ - function killBorrowVerify(address palPool, address killer, address loanAddress) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + function killBorrowVerify(address palPool, address killer, address loanAddress) external override returns(bool){ + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); - palPool; killer; - loanAddress; + + //Accrue Rewards to the Loan's owner + if(autoBorrowRewards[palPool]) accrueBorrowRewards(palPool, loanAddress); //Because some PalPool call this method as view + //So we need to know if the call can update storage or not + //If not, will need to use the Manual Borrow Reward claim system //no method yet return true; } + + + // PalToken Deposit/Withdraw functions + + function deposit(address palToken, uint amount) external override returns(bool){ + address palPool = palTokenToPalPool[palToken]; + address user = msg.sender; + IERC20 token = IERC20(palToken); + + require(amount <= token.balanceOf(user), Errors.INSUFFICIENT_BALANCE); + + updateSupplyIndex(palPool); + accrueSupplyRewards(palPool, user); + + supplierDeposits[palPool][user] = supplierDeposits[palPool][user].add(amount); + totalSupplierDeposits[palPool] = totalSupplierDeposits[palPool].add(amount); + + token.safeTransferFrom(user, address(this), amount); + + emit Deposit(user, palToken, amount); + + return true; + } + + + function withdraw(address palToken, uint amount) external override returns(bool){ + address palPool = palTokenToPalPool[palToken]; + address user = msg.sender; + + require(amount <= supplierDeposits[palPool][user], Errors.INSUFFICIENT_DEPOSITED); + + updateSupplyIndex(palPool); + accrueSupplyRewards(palPool, user); + + IERC20 token = IERC20(palToken); + + supplierDeposits[palPool][user] = supplierDeposits[palPool][user].sub(amount); + totalSupplierDeposits[palPool] = totalSupplierDeposits[palPool].sub(amount); + + token.safeTransfer(user, amount); + + emit Withdraw(user, palToken, amount); + + return true; + } + + + // Rewards functions + + /** + * @notice Internal - Updates the Supply Index of a Pool for reward distribution + * @param palPool address of the Pool to update the Supply Index for + */ + function updateSupplyIndex(address palPool) internal { + // Get last Pool Supply Rewards state + PoolRewardsState storage state = supplyRewardState[palPool]; + // Get the current block number, and the Supply Speed for the given Pool + uint currentBlock = block.number; + uint supplySpeed = supplySpeeds[palPool]; + + // Calculate the number of blocks since last update + uint ellapsedBlocks = currentBlock.sub(uint(state.blockNumber)); + + // If an update is needed : block ellapsed & non-null speed (rewards to distribute) + if(ellapsedBlocks > 0 && supplySpeed > 0){ + // Get the Total Amount deposited in the Controller of PalToken associated to the Pool + uint totalDeposited = totalSupplierDeposits[palPool]; + + // Calculate the amount of rewards token accrued since last update + uint accruedAmount = ellapsedBlocks.mul(supplySpeed); + + // And the new ratio for reward distribution to user + // Based on the amount of rewards accrued, and the change in the TotalSupply + uint ratio = totalDeposited > 0 ? accruedAmount.mul(1e36).div(totalDeposited) : 0; + + // Write new Supply Rewards values in the storage + state.index = safe224(uint(state.index).add(ratio)); + state.blockNumber = safe32(currentBlock); + } + else if(ellapsedBlocks > 0){ + // If blocks ellapsed, but no rewards to distribute (speed == 0), + // just write the last update block number + state.blockNumber = safe32(currentBlock); + } + + } + + /** + * @notice Internal - Accrues rewards token to the user claimable balance, depending on the Pool SupplyRewards state + * @param palPool address of the PalPool the user interracted with + * @param user address of the user to accrue rewards to + */ + function accrueSupplyRewards(address palPool, address user) internal { + // Get the Pool current SupplyRewards state + PoolRewardsState storage state = supplyRewardState[palPool]; + + // Get the current reward index for the Pool + // And the user last reward index + uint currentSupplyIndex = state.index; + uint userSupplyIndex = supplierRewardIndex[palPool][user]; + + // Update the Index in the mapping, the local value is used after + supplierRewardIndex[palPool][user] = currentSupplyIndex; + + if(userSupplyIndex == 0 && currentSupplyIndex >= initialRewardsIndex){ + // Set the initial Index for the user + userSupplyIndex = initialRewardsIndex; + } + + // Get the difference of index with the last one for user + uint indexDiff = currentSupplyIndex.sub(userSupplyIndex); + + if(indexDiff > 0){ + // And using the user PalToken balance deposited in the Controller, + // we can get how much rewards where accrued + uint userBalance = supplierDeposits[palPool][user]; + + uint userAccruedRewards = userBalance.mul(indexDiff).div(1e36); + + // Add the new amount of rewards to the user total claimable balance + accruedRewards[user] = accruedRewards[user].add(userAccruedRewards); + } + + } + + /** + * @notice Internal - Saves the BorrowRewards Ratio for a PalLoan, depending on the PalPool + * @param palPool address of the PalPool the Loan comes from + * @param loanAddress address of the PalLoan contract + */ + function setLoanBorrowRewards(address palPool, address loanAddress) internal { + // Saves the current Borrow Reward Ratio to use for that Loan rewards at Closing/Killing + loansBorrowRatios[loanAddress] = borrowRatios[palPool]; + } + + /** + * @notice Internal - Accrues reward to the PalLoan owner when the Loan is closed (if the auto-accrue rewards is on for the Pool) + * @param palPool address of the PalPool the Loan comes from + * @param loanAddress address of the PalLoan contract + */ + function accrueBorrowRewards(address palPool, address loanAddress) internal { + // Get the PalLoan BorrowRatio for rewards + uint loanBorrowRatio = loansBorrowRatios[loanAddress]; + + // Skip if no rewards set for the PalLoan OR if rewards were already claimed for the PalLoan + if(loanBorrowRatio > 0 && !isLoanRewardClaimed[loanAddress]){ + IPalPool pool = IPalPool(palPool); + + // Get the Borrower, and the amount of fees used by the Loan + // And using the borrowRatio, accrue rewards for the borrower + // The amount ot be accrued is calculated as feesUsed * borrowRatio + (address borrower,,,,,,,uint feesUsedAmount,,,,) = pool.getBorrowData(loanAddress); + + uint userAccruedRewards = feesUsedAmount.mul(loanBorrowRatio).div(1e18); + + // Add the new amount of rewards to the user total claimable balance + accruedRewards[borrower] = accruedRewards[borrower].add(userAccruedRewards); + + // Set this Loan rewards as claimed/distributed + isLoanRewardClaimed[loanAddress] = true; + } + + } + + + function _calculateLoanRewards(address palPool, address loanAddress) internal view returns(uint){ + // Rewards already claimed + if(isLoanRewardClaimed[loanAddress]) return 0; + + IPalPool pool = IPalPool(palPool); + (,,,,,,,uint feesUsedAmount,uint loanStartBlock,,bool closed,) = pool.getBorrowData(loanAddress); + + // Need the Loan to be closed before accruing rewards + if(!closed) return 0; + + // Loan as taken before Borrow Rewards were set + if(borrowRewardsStartBlock[palPool] == 0 || borrowRewardsStartBlock[palPool] > loanStartBlock) return 0; + + // Calculate the amount of rewards based on the Pool ratio & the amount of usedFees in the Loan + uint poolBorrowRatio = loansBorrowRatios[loanAddress] > 0 ? loansBorrowRatios[loanAddress] : borrowRatios[palPool]; + + return feesUsedAmount.mul(poolBorrowRatio).div(1e18); + } + + /** + * @notice Returns the current amount of reward tokens the user can claim for a PalLoan + * @param palPool address of the PalPool + * @param loanAddress address of the PalLoan + */ + function claimableLoanRewards(address palPool, address loanAddress) external view override returns(uint) { + return _calculateLoanRewards(palPool, loanAddress); + } + + function claimLoanRewards(address palPool, address loanAddress) external override { + IPalPool pool = IPalPool(palPool); + (address borrower,,,,,,,,,,bool closed,) = pool.getBorrowData(loanAddress); + + // Check if the PalLoan has some claimable rewards, and if it was not claimed already + require(msg.sender == borrower, Errors.NOT_LOAN_OWNER); + uint claimableAmount = _calculateLoanRewards(palPool, loanAddress); + + require(closed && claimableAmount != 0, Errors.NOT_CLAIMABLE); + + // Set this Loan rewards as claimed/distributed + isLoanRewardClaimed[loanAddress] = true; + + // Transfer the rewards to the borrower + IERC20 token = IERC20(rewardToken()); + require(claimableAmount <= token.balanceOf(address(this)), Errors.REWARDS_CASH_TOO_LOW); + + token.transfer(borrower, claimableAmount); + + emit ClaimRewards(borrower, claimableAmount); + + } + + /** + * @notice Returns the current amount of reward tokens the user can claim + * @param user address of user + */ + function claimable(address user) external view override returns(uint) { + return accruedRewards[user]; + } + + /** + * @notice Returns the current amount of reward tokens the user can claim + the estimation from last Supplier Index updates + * @param user address of user + */ + function estimateClaimable(address user) external view override returns(uint){ + //All the rewards already accrued and not claimed + uint _total = accruedRewards[user]; + + //Calculate the estimated pending rewards for all Pools for the user + //(depending on the last Pool's updateSupplyIndex) + address[] memory _pools = palPools; + for(uint i = 0; i < _pools.length; i++){ + // Get the current reward index for the Pool + // And the user last reward index + uint currentSupplyIndex = supplyRewardState[_pools[i]].index; + uint userSupplyIndex = supplierRewardIndex[_pools[i]][user]; + + if(userSupplyIndex == 0 && currentSupplyIndex >= initialRewardsIndex){ + // Set the initial Index for the user + userSupplyIndex = initialRewardsIndex; + } + + // Get the difference of index with the last one for user + uint indexDiff = currentSupplyIndex.sub(userSupplyIndex); + + if(indexDiff > 0){ + // And using the user PalToken balance deposited in the Controller, + // we can get how much rewards where accrued + uint userBalance = supplierDeposits[_pools[i]][user]; + + uint userAccruedRewards = userBalance.mul(indexDiff).div(1e36); + + // Add the new amount of rewards to the user total claimable balance + _total = _total.add(userAccruedRewards); + } + } + + return _total; + } + + /** + * @notice Update the claimable rewards for a given user + * @param user address of user + */ + function updateUserRewards(address user) public override { + address[] memory _pools = palPools; + for(uint i = 0; i < _pools.length; i++){ + // Need to update the Supply Index + updateSupplyIndex(_pools[i]); + // To then accrue the user rewards for that Pool + //set at 0 & true for amount & positive, since no change in user LP position + accrueSupplyRewards(_pools[i], user); + // No need to do it for the Borrower rewards + } + } + + /** + * @notice Accrues rewards for the user, then send all rewards tokens claimable + * @param user address of user + */ + function claim(address user) external override { + // Accrue any claimable rewards for all the Pools for the user + updateUserRewards(user); + + // Get current amount claimable for the user + uint toClaim = accruedRewards[user]; + + // If there is a claimable amount + if(toClaim > 0){ + IERC20 token = IERC20(rewardToken()); + require(toClaim <= token.balanceOf(address(this)), Errors.REWARDS_CASH_TOO_LOW); + + // All rewards were accrued and sent to the user, reset the counter + accruedRewards[user] = 0; + + // Transfer the tokens to the user + token.transfer(user, toClaim); + + emit ClaimRewards(user, toClaim); + } + } + + /** + * @notice Returns the global Supply distribution speed + * @return uint : Total Speed + */ + function totalSupplyRewardSpeed() external view override returns(uint) { + // Sum up the SupplySpeed for all the listed PalPools + address[] memory _pools = palPools; + uint totalSpeed = 0; + for(uint i = 0; i < _pools.length; i++){ + totalSpeed = totalSpeed.add(supplySpeeds[_pools[i]]); + } + return totalSpeed; + } + + + + /** @notice Address of the reward Token (PAL token) */ + function rewardToken() public view returns(address) { + return rewardTokenAddress; + } //Admin function + function becomeImplementation(ControllerProxy proxy) external override adminOnly { + // Only to call after the contract was set as Pending Implementation in the Proxy contract + // To accept the delegatecalls, and update the Implementation address in the Proxy + require(proxy.acceptImplementation(), Errors.FAIL_BECOME_IMPLEMENTATION); + } + + function updateRewardToken(address newRewardTokenAddress) external override adminOnly { + rewardTokenAddress = newRewardTokenAddress; + } + - function setPoolsNewController(address _newController) external override adminOnly returns(bool){ + function setPoolsNewController(address newController) external override adminOnly returns(bool){ address[] memory _pools = palPools; for(uint i = 0; i < _pools.length; i++){ IPalPool _palPool = IPalPool(_pools[i]); - _palPool.setNewController(_newController); + _palPool.setNewController(newController); } return true; } - function withdrawFromPool(address _pool, uint _amount, address _recipient) external override adminOnly returns(bool){ - IPalPool _palPool = IPalPool(_pool); - _palPool.withdrawFees(_amount, _recipient); + function withdrawFromPool(address pool, uint amount, address recipient) external override adminOnly returns(bool){ + IPalPool _palPool = IPalPool(pool); + _palPool.withdrawFees(amount, recipient); return true; } + // set a pool rewards values (admin) + function updatePoolRewards(address palPool, uint newSupplySpeed, uint newBorrowRatio, bool autoBorrowReward) external override adminOnly { + require(isPalPool(palPool), Errors.POOL_NOT_LISTED); + + if(newSupplySpeed != supplySpeeds[palPool]){ + //Make sure it's updated before setting the new speed + updateSupplyIndex(palPool); + + supplySpeeds[palPool] = newSupplySpeed; + } + + if(newBorrowRatio != borrowRatios[palPool]){ + borrowRatios[palPool] = newBorrowRatio; + } + + if(borrowRewardsStartBlock[palPool] == 0 && newBorrowRatio != 0){ + borrowRewardsStartBlock[palPool] = block.number; + } + else if(newBorrowRatio == 0){ + borrowRewardsStartBlock[palPool] = 0; + } + + autoBorrowRewards[palPool] = autoBorrowReward; + + emit PoolRewardsUpdated(palPool, newSupplySpeed, newBorrowRatio, autoBorrowReward); + } + + // (admin) send all unclaimed/non-accrued rewards to other contract / to multisig / to admin ? + + + + //Math utils + + function safe224(uint n) internal pure returns (uint224) { + require(n < 2**224, "Number is over 224 bits"); + return uint224(n); + } + + function safe32(uint n) internal pure returns (uint32) { + require(n < 2**32, "Number is over 32 bits"); + return uint32(n); + } + } \ No newline at end of file diff --git a/contracts/tests/MockPalPool.sol b/contracts/tests/MockPalPool.sol index 7ca3f84..2de2f27 100644 --- a/contracts/tests/MockPalPool.sol +++ b/contracts/tests/MockPalPool.sol @@ -15,6 +15,7 @@ contract MockPalPool is Admin { uint public totalReserve; uint public accruedFees; IPaladinController public controller; + bool public lastControllerCallResult; modifier controllerOnly() { require(msg.sender == admin || msg.sender == address(controller), Errors.CALLER_NOT_CONTROLLER); @@ -50,4 +51,28 @@ contract MockPalPool is Admin { function setNewController(address _newController) external controllerOnly { controller = IPaladinController(_newController); } + + function testDepositVerify(address palPool, address dest, uint amount) public { + lastControllerCallResult = controller.depositVerify(palPool, dest, amount); + } + + function testWithdrawVerify(address palPool, address dest, uint amount) public { + lastControllerCallResult = controller.withdrawVerify(palPool, dest, amount); + } + + function testBorrowVerify(address palPool, address borrower, address delegatee, uint amount, uint feesAmount, address loanAddress) public { + lastControllerCallResult = controller.borrowVerify(palPool, borrower, delegatee, amount, feesAmount, loanAddress); + } + + function testExpandBorrowVerify(address palPool, address loanAddress, uint newFeesAmount) public { + lastControllerCallResult = controller.expandBorrowVerify(palPool, loanAddress, newFeesAmount); + } + + function testCloseBorrowVerify(address palPool, address borrower, address loanAddress) public { + lastControllerCallResult = controller.closeBorrowVerify(palPool, borrower, loanAddress); + } + + function testKillBorrowVerify(address palPool, address killer, address loanAddress) public { + lastControllerCallResult = controller.killBorrowVerify(palPool, killer, loanAddress); + } } \ No newline at end of file diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index de69c0f..e4f253b 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -15,6 +15,7 @@ library Errors { string public constant CALLER_NOT_CONTROLLER = '29'; // 'The caller must be the admin or the controller' string public constant CALLER_NOT_ALLOWED_POOL = '30'; // 'The caller must be a palPool listed in the controller' string public constant CALLER_NOT_MINTER = '31'; + string public constant CALLER_NOT_IMPLEMENTATION = '35'; // 'The caller must be the pending Implementation' // ERC20 type errors string public constant FAIL_TRANSFER = '2'; @@ -48,4 +49,15 @@ library Errors { string public constant FAIL_LOAN_TOKEN_BURN = '33'; string public constant FEES_ACCRUED_INSUFFICIENT = '34'; + + //Controller errors + string public constant LIST_SIZES_NOT_EQUAL = '36'; + string public constant POOL_LIST_ALREADY_SET = '37'; + string public constant POOL_ALREADY_LISTED = '38'; + string public constant POOL_NOT_LISTED = '39'; + string public constant CALLER_NOT_POOL = '40'; + string public constant REWARDS_CASH_TOO_LOW = '41'; + string public constant FAIL_BECOME_IMPLEMENTATION = '42'; + string public constant INSUFFICIENT_DEPOSITED = '43'; + string public constant NOT_CLAIMABLE = '44'; } \ No newline at end of file diff --git a/contracts/variants/DoomsdayController.sol b/contracts/variants/DoomsdayController.sol index 61206ff..3af7c48 100644 --- a/contracts/variants/DoomsdayController.sol +++ b/contracts/variants/DoomsdayController.sol @@ -11,22 +11,15 @@ pragma solidity ^0.7.6; import "../utils/SafeMath.sol"; import "../IPaladinController.sol"; +import "../ControllerStorage.sol"; import "../PalPool.sol"; import "../IPalPool.sol"; import "../utils/IERC20.sol"; -import "../utils/Admin.sol"; /** @title DoomsdayController contract -> blocks any transaction from the PalPools */ /// @author Paladin -contract DoomsdayController is IPaladinController, Admin { +contract DoomsdayController is IPaladinController, ControllerStorage { using SafeMath for uint; - - - /** @notice List of current active palToken Pools */ - address[] public palTokens; - address[] public palPools; - - bool private initialized = false; constructor(){ admin = msg.sender; @@ -69,11 +62,24 @@ contract DoomsdayController is IPaladinController, Admin { * @return bool : Success */ function setInitialPools(address[] memory _palTokens, address[] memory _palPools) external override adminOnly returns(bool){ - require(!initialized, "Lists already set"); - require(_palTokens.length == _palPools.length, "List sizes not equal"); + require(!initialized, Errors.POOL_LIST_ALREADY_SET); + require(_palTokens.length == _palPools.length, Errors.LIST_SIZES_NOT_EQUAL); palPools = _palPools; palTokens = _palTokens; initialized = true; + + for(uint i = 0; i < _palPools.length; i++){ + //Update the Reward State for the new Pool + PoolRewardsState storage supplyState = supplyRewardState[_palPools[i]]; + if(supplyState.index == 0){ + supplyState.index = initialRewardsIndex; + } + supplyState.blockNumber = safe32(block.number); + + //Link PalToken with PalPool + palTokenToPalPool[_palTokens[i]] = _palPools[i]; + } + return true; } @@ -97,7 +103,7 @@ contract DoomsdayController is IPaladinController, Admin { */ function removePool(address _palPool) external override adminOnly returns(bool){ //Remove a palToken & palPool from the list - require(isPalPool(_palPool), "Not listed"); + require(isPalPool(_palPool), Errors.POOL_NOT_LISTED); address[] memory _pools = palPools; @@ -105,7 +111,9 @@ contract DoomsdayController is IPaladinController, Admin { for(uint i = 0; i < _pools.length; i++){ if(_pools[i] == _palPool){ //get the address of the PalToken for the Event - address _palToken = _pools[i]; + address _palToken = palTokens[i]; + + delete palTokenToPalPool[_palToken]; //Replace the address to remove with the last one of the array palPools[i] = palPools[lastIndex]; @@ -158,7 +166,7 @@ contract DoomsdayController is IPaladinController, Admin { * @return bool : Verification Success */ function depositVerify(address palPool, address dest, uint amount) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); palPool; dest; @@ -177,7 +185,7 @@ contract DoomsdayController is IPaladinController, Admin { * @return bool : Verification Success */ function withdrawVerify(address palPool, address dest, uint amount) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); palPool; dest; @@ -198,7 +206,7 @@ contract DoomsdayController is IPaladinController, Admin { * @return bool : Verification Success */ function borrowVerify(address palPool, address borrower, address delegatee, uint amount, uint feesAmount, address loanPool) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); palPool; borrower; @@ -218,7 +226,7 @@ contract DoomsdayController is IPaladinController, Admin { * @return bool : Verification Success */ function expandBorrowVerify(address palPool, address loanAddress, uint newFeesAmount) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); palPool; loanAddress; @@ -232,15 +240,16 @@ contract DoomsdayController is IPaladinController, Admin { * @notice Check if Borrow Closing was correctly done * @param palPool address of PalPool * @param borrower borrower's address - * @param loanPool address of the PalLoan contract to close + * @param loanAddress address of the PalLoan contract to close * @return bool : Verification Success */ - function closeBorrowVerify(address palPool, address borrower, address loanPool) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + function closeBorrowVerify(address palPool, address borrower, address loanAddress) external override returns(bool){ + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); - palPool; borrower; - loanPool; + + //Accrue Rewards to the Loan's owner + accrueBorrowRewards(palPool, loanAddress); //no method yet return true; @@ -251,24 +260,277 @@ contract DoomsdayController is IPaladinController, Admin { * @notice Check if Borrow Killing was correctly done * @param palPool address of PalPool * @param killer killer's address - * @param loanPool address of the PalLoan contract to kill + * @param loanAddress address of the PalLoan contract to kill * @return bool : Verification Success */ - function killBorrowVerify(address palPool, address killer, address loanPool) external view override returns(bool){ - require(isPalPool(msg.sender), "Call not allowed"); + function killBorrowVerify(address palPool, address killer, address loanAddress) external override returns(bool){ + require(isPalPool(msg.sender), Errors.CALLER_NOT_POOL); - palPool; killer; - loanPool; + + //Accrue Rewards to the Loan's owner + accrueBorrowRewards(palPool, loanAddress); //no method yet return true; } + + // PalToken Deposit/Withdraw functions + + function deposit(address palToken, uint amount) external pure override returns(bool){ + palToken; + amount; + revert(); + } + + + function withdraw(address palToken, uint amount) external pure override returns(bool){ + palToken; + amount; + revert(); + } + + // Rewards functions + + /** + * @notice Internal - Updates the Supply Index of a Pool for reward distribution + * @param palPool address of the Pool to update the Supply Index for + */ + function updateSupplyIndex(address palPool) internal { + // Get last Pool Supply Rewards state + PoolRewardsState storage state = supplyRewardState[palPool]; + // Get the current block number, and the Supply Speed for the given Pool + uint currentBlock = block.number; + uint supplySpeed = supplySpeeds[palPool]; + + // Calculate the number of blocks since last update + uint ellapsedBlocks = currentBlock.sub(uint(state.blockNumber)); + + // If an update is needed : block ellapsed & non-null speed (rewards to distribute) + if(ellapsedBlocks > 0 && supplySpeed > 0){ + // Get the Total Amount deposited in the Controller of PalToken associated to the Pool + uint totalDeposited = totalSupplierDeposits[palPool]; + + // Calculate the amount of rewards token accrued since last update + uint accruedAmount = ellapsedBlocks.mul(supplySpeed); + + // And the new ratio for reward distribution to user + // Based on the amount of rewards accrued, and the change in the TotalSupply + uint ratio = totalDeposited > 0 ? accruedAmount.mul(1e36).div(totalDeposited) : 0; + + // Write new Supply Rewards values in the storage + state.index = safe224(uint(state.index).add(ratio)); + state.blockNumber = safe32(currentBlock); + } + else if(ellapsedBlocks > 0){ + // If blocks ellapsed, but no rewards to distribute (speed == 0), + // just write the last update block number + state.blockNumber = safe32(currentBlock); + } + + } + + /** + * @notice Internal - Accrues rewards token to the user claimable balance, depending on the Pool SupplyRewards state + * @param palPool address of the PalPool the user interracted with + * @param user address of the user to accrue rewards to + */ + function accrueSupplyRewards(address palPool, address user) internal { + // Get the Pool current SupplyRewards state + PoolRewardsState storage state = supplyRewardState[palPool]; + + // Get the current reward index for the Pool + // And the user last reward index + uint currentSupplyIndex = state.index; + uint userSupplyIndex = supplierRewardIndex[palPool][user]; + + // Update the Index in the mapping, the local value is used after + supplierRewardIndex[palPool][user] = currentSupplyIndex; + + if(userSupplyIndex == 0 && currentSupplyIndex >= initialRewardsIndex){ + // Set the initial Index for the user + userSupplyIndex = initialRewardsIndex; + } + + // Get the difference of index with the last one for user + uint indexDiff = currentSupplyIndex.sub(userSupplyIndex); + + if(indexDiff > 0){ + // And using the user PalToken balance deposited in the Controller, + // we can get how much rewards where accrued + uint userBalance = supplierDeposits[palPool][user]; + + uint userAccruedRewards = userBalance.mul(indexDiff).div(1e36); + + // Add the new amount of rewards to the user total claimable balance + accruedRewards[user] = accruedRewards[user].add(userAccruedRewards); + } + + } + + /** + * @notice Internal - Accrues reward to the PalLoan owner when the Loan is closed + * @param palPool address of the PalPool the Loan comes from + * @param loanAddress address of the PalLoan contract + */ + function accrueBorrowRewards(address palPool, address loanAddress) internal { + // Get the Pool BorrowRatio for rewards + uint poolBorrowRatio = borrowRatios[palPool]; + + // Skip if no rewards set for the Pool + if(poolBorrowRatio > 0){ + IPalPool pool = IPalPool(palPool); + + // Get the Borrower, and the amount of fees used by the Loan + // And using the borrowRatio, accrue rewards for the borrower + // The amount ot be accrued is calculated as feesUsed * borrowRatio + address borrower; + uint feesUsedAmount; + + (borrower,,,,,,,feesUsedAmount,,,,) = pool.getBorrowData(loanAddress); + + uint userAccruedRewards = feesUsedAmount.mul(feesUsedAmount).div(1e18); + + // Add the new amount of rewards to the user total claimable balance + accruedRewards[borrower] = accruedRewards[borrower].add(userAccruedRewards); + } + + } + + function _calculateLoanRewards(address palPool, address loanAddress) internal view returns(uint){ + // Rewards already claimed + if(isLoanRewardClaimed[loanAddress]) return 0; + + IPalPool pool = IPalPool(palPool); + (,,,,,,,uint feesUsedAmount,uint loanStartBlock,,bool closed,) = pool.getBorrowData(loanAddress); + + // Need the Loan to be closed before accruing rewards + if(!closed) return 0; + + // Loan as taken before Borrow Rewards were set + if(borrowRewardsStartBlock[palPool] == 0 || borrowRewardsStartBlock[palPool] > loanStartBlock) return 0; + + // Calculate the amount of rewards based on the Pool ratio & the amount of usedFees in the Loan + uint poolBorrowRatio = loansBorrowRatios[loanAddress] > 0 ? loansBorrowRatios[loanAddress] : borrowRatios[palPool]; + + return feesUsedAmount.mul(poolBorrowRatio).div(1e18); + } + + /** + * @notice Returns the current amount of reward tokens the user can claim for a PalLoan + * @param palPool address of the PalPool + * @param loanAddress address of the PalLoan + */ + function claimableLoanRewards(address palPool, address loanAddress) external view override returns(uint) { + return _calculateLoanRewards(palPool, loanAddress); + } + + function claimLoanRewards(address palPool, address loanAddress) external override { + palPool; + loanAddress; + revert(); + } + + /** + * @notice Returns the current amount of reward tokens the user can claim + * @param user address of user + */ + function claimable(address user) external view override returns(uint) { + return accruedRewards[user]; + } + + /** + * @notice Returns the current amount of reward tokens the user can claim + the estimation from last Supplier Index updates + * @param user address of user + */ + function estimateClaimable(address user) external view override returns(uint){ + //All the rewards already accrued and not claimed + uint _total = accruedRewards[user]; + + //Calculate the estimated pending rewards for all Pools for the user + //(depending on the last Pool's updateSupplyIndex) + address[] memory _pools = palPools; + for(uint i = 0; i < _pools.length; i++){ + // Get the current reward index for the Pool + // And the user last reward index + uint currentSupplyIndex = supplyRewardState[_pools[i]].index; + uint userSupplyIndex = supplierRewardIndex[_pools[i]][user]; + + if(userSupplyIndex == 0 && currentSupplyIndex >= initialRewardsIndex){ + // Set the initial Index for the user + userSupplyIndex = initialRewardsIndex; + } + + // Get the difference of index with the last one for user + uint indexDiff = currentSupplyIndex.sub(userSupplyIndex); + + if(indexDiff > 0){ + // And using the user PalToken balance deposited in the Controller, + // we can get how much rewards where accrued + uint userBalance = supplierDeposits[_pools[i]][user]; + + uint userAccruedRewards = userBalance.mul(indexDiff).div(1e36); + + // Add the new amount of rewards to the user total claimable balance + _total = _total.add(userAccruedRewards); + } + } + + return _total; + } + + /** + * @notice Update the claimable rewards for a given user + * @param user address of user + */ + function updateUserRewards(address user) external override { + user; + revert(); + } + + /** + * @notice Accrues rewards for the user, then send all rewards tokens claimable + * @param user address of user + */ + function claim(address user) external override { + user; + revert(); + } + + /** + * @notice Returns the global Supply distribution speed + * @return uint : Total Speed + */ + function totalSupplyRewardSpeed() external view override returns(uint) { + // Sum up the SupplySpeed for all the listed PalPools + address[] memory _pools = palPools; + uint totalSpeed = 0; + for(uint i = 0; i < _pools.length; i++){ + totalSpeed = totalSpeed.add(supplySpeeds[_pools[i]]); + } + return totalSpeed; + } + + + /** @notice Address of the reward Token (PAL token) */ + function rewardToken() public view returns(address) { + return rewardTokenAddress; + } + //Admin function + function becomeImplementation(ControllerProxy proxy) external override adminOnly { + // Only to call after the contract was set as Pending Implementation in the Proxy contract + // To accept the delegatecalls, and update the Implementation address in the Proxy + require(proxy.acceptImplementation(), Errors.FAIL_BECOME_IMPLEMENTATION); + } + + function updateRewardToken(address newRewardTokenAddress) external override adminOnly { + rewardTokenAddress = newRewardTokenAddress; + } function setPoolsNewController(address _newController) external override adminOnly returns(bool){ address[] memory _pools = palPools; @@ -286,5 +548,45 @@ contract DoomsdayController is IPaladinController, Admin { return true; } + // set a pool rewards values (admin) + function updatePoolRewards(address palPool, uint newSupplySpeed, uint newBorrowRatio, bool autoBorrowReward) external override adminOnly { + require(isPalPool(palPool), Errors.POOL_NOT_LISTED); + + if(newSupplySpeed != supplySpeeds[palPool]){ + //Make sure it's updated before setting the new speed + updateSupplyIndex(palPool); + + supplySpeeds[palPool] = newSupplySpeed; + } + + if(newBorrowRatio != borrowRatios[palPool]){ + borrowRatios[palPool] = newBorrowRatio; + } + + if(borrowRewardsStartBlock[palPool] == 0 && newBorrowRatio != 0){ + borrowRewardsStartBlock[palPool] = block.number; + } + else if(newBorrowRatio == 0){ + borrowRewardsStartBlock[palPool] = 0; + } + + autoBorrowRewards[palPool] = autoBorrowReward; + + emit PoolRewardsUpdated(palPool, newSupplySpeed, newBorrowRatio, autoBorrowReward); + } + + + //Math utils + + function safe224(uint n) internal pure returns (uint224) { + require(n < 2**224, "Number is over 224 bits"); + return uint224(n); + } + + function safe32(uint n) internal pure returns (uint32) { + require(n < 2**32, "Number is over 32 bits"); + return uint32(n); + } + } \ No newline at end of file diff --git a/scripts/deploy/all_deploy.ts b/scripts/deploy/all_deploy.ts index 3f103cf..7c17e3c 100644 --- a/scripts/deploy/all_deploy.ts +++ b/scripts/deploy/all_deploy.ts @@ -51,6 +51,7 @@ async function main() { const Controller = await ethers.getContractFactory("PaladinController"); + const Proxy = await ethers.getContractFactory("ControllerProxy"); const Interest = await ethers.getContractFactory("InterestCalculatorV2"); const PalLoanToken = await ethers.getContractFactory("PalLoanToken"); const BurnedPalLoanToken = await ethers.getContractFactory("BurnedPalLoanToken"); @@ -66,11 +67,16 @@ async function main() { const AaveMultiplier = await ethers.getContractFactory("AaveMultiplier"); - console.log('Deploying the Paladin Controller ...') + console.log('Deploying the Paladin Controller (Implementation & Proxy) ...') + console.log('Deploying Implementation ...') const controller = await Controller.deploy(); await controller.deployTransaction.wait(5); + console.log('Deploying Proxy ...') + const proxy = await Proxy.deploy(); + await proxy.deployTransaction.wait(5); + console.log('Deploying the Interest Calculator Module V2 ...') const interest = await Interest.deploy(); @@ -78,13 +84,20 @@ async function main() { await Promise.all([ controller.deployed(), + proxy.deployed(), interest.deployed(), ]); + console.log('Setting Implementation for Proxy ...') + console.log('Propose Implementation ...') + await proxy.proposeImplementation(controller.address) + console.log('Accept Implementation ...') + await controller.becomeImplementation(proxy.address) + console.log('Deploying PalLoanToken (ERC721) contract ...') - const loanToken = await PalLoanToken.deploy(controller.address, PAL_LOAN_TOKEN_URI); + const loanToken = await PalLoanToken.deploy(proxy.address, PAL_LOAN_TOKEN_URI); await loanToken.deployed(); @@ -119,7 +132,7 @@ async function main() { if (params.SYMBOL === 'palStkAAVE') { palPool = await stkAavePalPool.deploy( palToken.address, - controller.address, + proxy.address, params.UNDERLYING, interest.address, delegators[params.DELEGATOR], @@ -130,7 +143,7 @@ async function main() { else { palPool = await PalPool.deploy( palToken.address, - controller.address, + proxy.address, params.UNDERLYING, interest.address, delegators[params.DELEGATOR], @@ -161,7 +174,8 @@ async function main() { } - const tx = await controller.setInitialPools(tokens, pools); + const poxy_controller_interface = Controller.attach(proxy.address); + const tx = await poxy_controller_interface.setInitialPools(tokens, pools); await tx.wait(15); @@ -196,7 +210,7 @@ async function main() { console.log('Deploying the Address Registry ...') const registry = await Registry.deploy( - controller.address, + proxy.address, loanToken.address, underlyings, pools, @@ -233,6 +247,10 @@ async function main() { address: controller.address, constructorArguments: [], }); + await hre.run("verify:verify", { + address: proxy.address, + constructorArguments: [], + }); console.log() await interest.deployTransaction.wait(5); await hre.run("verify:verify", { @@ -333,6 +351,9 @@ async function main() { console.log('Controller : ') console.log(controller.address) console.log() + console.log('Controller Proxy : ') + console.log(proxy.address) + console.log() console.log('Interest Calculator : ') console.log(interest.address) console.log() diff --git a/scripts/deploy/controller_and_proxy_deploy.ts b/scripts/deploy/controller_and_proxy_deploy.ts new file mode 100644 index 0000000..658c9e8 --- /dev/null +++ b/scripts/deploy/controller_and_proxy_deploy.ts @@ -0,0 +1,90 @@ +export { }; +const hre = require("hardhat"); +const ethers = hre.ethers; + +const network = hre.network.name; +const params_path = () => { + if (network === 'kovan') { + return '../utils/kovan_params' + } + else if (network === 'rinkeby') { + return '../utils/rinkeby_params' + } + else { + return '../utils/main_params' + } +} + +const param_file_path = params_path(); + +const { POOLS } = require(param_file_path); + +let pools: String[] = []; +let tokens: String[] = []; + + +async function main() { + + console.log('Deploying a new Paladin Controller ...') + + const deployer = (await hre.ethers.getSigners())[0]; + + for (let p in POOLS) { + pools.push(POOLS[p].POOL) + tokens.push(POOLS[p].TOKEN) + } + + const Proxy = await ethers.getContractFactory("ControllerProxy"); + const Controller = await ethers.getContractFactory("PaladinController"); + + console.log('Deploy Implementation ...') + const controller = await Controller.deploy(); + await controller.deployed(); + + console.log('Deploy Proxy ...') + const proxy = await Proxy.deploy(); + await proxy.deployed(); + + await proxy.deployTransaction.wait(5); + + console.log('Setting Implementation for Proxy ...') + console.log('Propose Implementation ...') + let tx = await proxy.proposeImplementation(controller.address) + await tx.wait(15); + console.log('Accept Implementation ...') + tx = await controller.becomeImplementation(proxy.address) + await tx.wait(15); + + console.log('Setting up Pools ...') + const poxy_controller_interface = Controller.attach(proxy.address); + tx = await poxy_controller_interface.setInitialPools(tokens, pools); + await tx.wait(15); + + console.log('New Paladin Controller Implementation : ') + console.log(controller.address) + console.log('New Paladin Controller (proxy) : ') + console.log(proxy.address) + + await controller.deployTransaction.wait(30); + + await hre.run("verify:verify", { + address: controller.address, + constructorArguments: [], + }); + await hre.run("verify:verify", { + address: proxy.address, + constructorArguments: [], + }); + + +} + + +main() + .then(() => { + process.exit(0); + }) + .catch(error => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/scripts/deploy/controller_deploy.ts b/scripts/deploy/controller_deploy.ts index 6930a53..0199bdd 100644 --- a/scripts/deploy/controller_deploy.ts +++ b/scripts/deploy/controller_deploy.ts @@ -29,21 +29,21 @@ async function main() { const deployer = (await hre.ethers.getSigners())[0]; - for (let p in POOLS) { + /*for (let p in POOLS) { pools.push(POOLS[p].POOL) tokens.push(POOLS[p].TOKEN) - } + }*/ const Controller = await ethers.getContractFactory("PaladinController"); const controller = await Controller.deploy(); await controller.deployed(); - await controller.setInitialPools(tokens, pools); + //await controller.setInitialPools(tokens, pools); - console.log('New Paladin Controller : ') + console.log('New Paladin Controller Implementation : ') console.log(controller.address) - console.log('(controller address needs to be updated for PalPools & for PalLoanToken)') + console.log('(need to propose & accept for the Proxy)') await controller.deployTransaction.wait(30); diff --git a/scripts/deploy/proxy_deploy.ts b/scripts/deploy/proxy_deploy.ts new file mode 100644 index 0000000..d8e9d61 --- /dev/null +++ b/scripts/deploy/proxy_deploy.ts @@ -0,0 +1,69 @@ +export { }; +const hre = require("hardhat"); +const ethers = hre.ethers; + +const network = hre.network.name; +const params_path = () => { + if (network === 'kovan') { + return '../utils/kovan_params' + } + else if (network === 'rinkeby') { + return '../utils/rinkeby_params' + } + else { + return '../utils/main_params' + } +} + +const param_file_path = params_path(); + +const { POOLS } = require(param_file_path); + +let pools: String[] = []; +let tokens: String[] = []; + +async function main() { + + console.log('Deploying a new Paladin Controller ...') + + const deployer = (await hre.ethers.getSigners())[0]; + + for (let p in POOLS) { + pools.push(POOLS[p].POOL) + tokens.push(POOLS[p].TOKEN) + } + + const Proxy = await ethers.getContractFactory("ControllerProxy"); + const Controller = await ethers.getContractFactory("PaladinController"); + + const proxy = await Proxy.deploy(); + await proxy.deployed(); + + + + await proxy.setInitialPools(tokens, pools); + + console.log('New Paladin Controller (proxy) : ') + console.log(proxy.address) + console.log('(need to set implementation & accept it)') + console.log('(controller address needs to be updated for PalPools & for PalLoanToken)') + + await proxy.deployTransaction.wait(30); + + await hre.run("verify:verify", { + address: proxy.address, + constructorArguments: [], + }); + + +} + + +main() + .then(() => { + process.exit(0); + }) + .catch(error => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/scripts/utils/kovan_params.js b/scripts/utils/kovan_params.js index 37f5315..e6eb34c 100644 --- a/scripts/utils/kovan_params.js +++ b/scripts/utils/kovan_params.js @@ -106,20 +106,20 @@ const INTEREST_MODULE_VALUES = { const POOLS = { COMP: { - POOL: "0x2ad0827e1Fca16Cf937DaDedF5cE81Ed0848bE28", - TOKEN: "0xd7b24dCb2B250FAdEa3CfD175F502B9B55cB4A4D" + POOL: "0xa21fa099e94A2cF52Eb7425E02Bfff62d1E610C9", + TOKEN: "0xB2224F5653b2b5094E465e3f676479763a015916" }, UNI: { - POOL: "0xB1265A6B2C5d43Ff358A847DF64fD825b7ed70e0", - TOKEN: "0x961692fb4Ca983A116a6432E2b82972094c71cf2" + POOL: "0xca7924020aa36e3c8b4e16fC2ACF1BdeA4d6fb12", + TOKEN: "0xFE32e7B30de865882f0DcDA353D40c40969F4531" }, AAVE: { - POOL: "0x62364874FB078C1B839b3234038Bf394b3D29205", - TOKEN: "0xA4Bd05F52735CecB54f9A21AB61a9d7aD90077a1" + POOL: "0xd9Fe6DD7A09029710Cfd8660F2EcED1788a36beE", + TOKEN: "0xbeda4e6081E09F7B8dc2b79B33aB1c60bDFa6a0C" }, STKAAVE: { - POOL: "0xd1Fe8B9E5B038cdE233A7C60Ca8b70ff2164dAAB", - TOKEN: "0x875195F9A9b7605DD25c368eCF777e91a47601d7" + POOL: "0xeef13A28b0dBE30a4C6c128819B651697CE7961d", + TOKEN: "0xeF89a9C8DF770A8964c339AA1073FB97F13BB943" }, INDEX: { POOL: "0x8808ea9ad2b7E49D4FdB707166dEA20Dfd262a0e", @@ -127,15 +127,15 @@ const POOLS = { } } -const PAL_LOAN_TOKEN = "0x4F6c21302a0A4a50512c7dD58fDC2B2b4788f9D6"; +const PAL_LOAN_TOKEN = "0xc8c5396B471B4FE7eB08b5e145f4DbEcE7860068"; -const CONTROLLER = "0x12692B01B2c0fc42c7daaf0a9Cb8a0B198916156"; +const CONTROLLER = "0x957EEbF87f1adAD8F9862412f76247F305fe8F4d"; -const INTEREST_MODULE = "0x037Cea58aCd888115792bceEe7CAdba3d633860a"; +const INTEREST_MODULE = ""; -const INTEREST_MODULE_V2 = "0xd18886822b0269836a03BAD0058f497493f35cB9"; +const INTEREST_MODULE_V2 = "0x0A06DfeCBABECAa6887B0D8e680A45E9e86a9838"; -const ADDRESS_REGISTRY = "0x961FACC23c1dAee162b6ff142f416E814F2Dea25"; +const ADDRESS_REGISTRY = "0xD0d13c2790372ca63d5932E59d0c4e88e682D417"; const OLD_DELEGATORS = { BASIC_DELEGATOR: "0x60F1604c521dE75B1A1c8Ee48614F08BBd01bD3C", @@ -155,7 +155,7 @@ const MULTIPLIERS = { COMP: { NAME: MULTIPLIER_NAMES.GOVERNOR, CONTRACT: MULTIPLIER_CONTRACTS.COMP, - ADDRESS: "0x3Bd47B4F89C41d3C62c70274D44b5F60fBA0F016", + ADDRESS: "0xD634F6B19d2e63433adFEBe148Be8d0Fe90D2E06", POOLS: [ POOLS.COMP.POOL ] @@ -163,7 +163,7 @@ const MULTIPLIERS = { UNI: { NAME: MULTIPLIER_NAMES.GOVERNOR, CONTRACT: MULTIPLIER_CONTRACTS.UNI, - ADDRESS: "0xACC64387b282A0C26216324E28B8d3fDfbd24e34", + ADDRESS: "0x573504437a7D939E9920ce2E3CdBc448650337D9", POOLS: [ POOLS.UNI.POOL ] @@ -171,7 +171,7 @@ const MULTIPLIERS = { AAVE: { NAME: MULTIPLIER_NAMES.AAVE, CONTRACT: MULTIPLIER_CONTRACTS.AAVE, - ADDRESS: "0x399D7EB51675e7Cd10a9196Dc4f86adDb68e10C7", + ADDRESS: "0x1EE8F7508D0b0f71043e4941C4a4a0bD1b6c0ee2", POOLS: [ POOLS.AAVE.POOL, POOLS.STKAAVE.POOL diff --git a/test/modules/controller.test.ts b/test/controller/controller.test.ts similarity index 81% rename from test/modules/controller.test.ts rename to test/controller/controller.test.ts index 7ac0498..d933448 100644 --- a/test/modules/controller.test.ts +++ b/test/controller/controller.test.ts @@ -2,6 +2,7 @@ import { ethers, waffle } from "hardhat"; import chai from "chai"; import { solidity } from "ethereum-waffle"; import { PaladinController } from "../../typechain/PaladinController"; +import { ControllerProxy } from "../../typechain/ControllerProxy"; import { Comp } from "../../typechain/Comp"; import { MockPalPool } from "../../typechain/MockPalPool"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; @@ -11,6 +12,7 @@ chai.use(solidity); const { expect } = chai; let controllerFactory: ContractFactory +let controllerProxyFactory: ContractFactory let mockPoolFactory: ContractFactory let erc20Factory: ContractFactory @@ -20,6 +22,7 @@ describe('Paladin Controller contract tests', () => { let user1: SignerWithAddress let controller: PaladinController + let proxy: ControllerProxy let mockPool: MockPalPool let underlying: Comp @@ -34,6 +37,7 @@ describe('Paladin Controller contract tests', () => { before( async () => { controllerFactory = await ethers.getContractFactory("PaladinController"); + controllerProxyFactory = await ethers.getContractFactory("ControllerProxy"); mockPoolFactory = await ethers.getContractFactory("MockPalPool"); erc20Factory = await ethers.getContractFactory("Comp"); }) @@ -54,6 +58,12 @@ describe('Paladin Controller contract tests', () => { controller = (await controllerFactory.deploy()) as PaladinController; await controller.deployed(); + proxy = (await controllerProxyFactory.deploy()) as ControllerProxy; + await proxy.deployed(); + + await proxy.proposeImplementation(controller.address); + await controller.becomeImplementation(proxy.address); + underlying = (await erc20Factory.connect(admin).deploy(admin.address)) as Comp; await underlying.deployed(); @@ -83,6 +93,10 @@ describe('Paladin Controller contract tests', () => { expect(pools).not.to.contain(fakePool3.address) expect(tokens).not.to.contain(fakeToken3.address) + expect(await controller.palTokenToPalPool(fakeToken.address)).to.be.eq(fakePool.address) + expect(await controller.palTokenToPalPool(fakeToken2.address)).to.be.eq(fakePool2.address) + expect(await controller.palTokenToPalPool(fakeToken3.address)).to.be.eq(ethers.constants.AddressZero) + expect(await controller.isPalPool(fakePool.address)).to.be.true; expect(await controller.isPalPool(fakePool2.address)).to.be.true; expect(await controller.isPalPool(fakePool3.address)).to.be.false; @@ -99,7 +113,7 @@ describe('Paladin Controller contract tests', () => { await expect( controller.connect(admin).setInitialPools([fakeToken.address], [fakePool.address]) - ).to.be.revertedWith('Lists already set') + ).to.be.revertedWith('37') }); }); @@ -127,6 +141,10 @@ describe('Paladin Controller contract tests', () => { expect(await controller.isPalPool(fakePool.address)).to.be.true; expect(await controller.isPalPool(fakePool2.address)).to.be.true; expect(await controller.isPalPool(fakePool3.address)).to.be.true; + + expect(await controller.palTokenToPalPool(fakeToken.address)).to.be.eq(fakePool.address) + expect(await controller.palTokenToPalPool(fakeToken2.address)).to.be.eq(fakePool2.address) + expect(await controller.palTokenToPalPool(fakeToken3.address)).to.be.eq(fakePool3.address) }); it(' should not add same PalPool (& PalToken) twice', async () => { @@ -134,7 +152,7 @@ describe('Paladin Controller contract tests', () => { await expect( controller.connect(admin).addNewPool(fakeToken.address, fakePool.address) - ).to.be.revertedWith('Already added') + ).to.be.revertedWith('38') }); it(' should block non-admin to add PalPool', async () => { @@ -166,6 +184,10 @@ describe('Paladin Controller contract tests', () => { expect(await controller.isPalPool(fakePool.address)).to.be.true; expect(await controller.isPalPool(fakePool2.address)).to.be.false; expect(await controller.isPalPool(fakePool3.address)).to.be.true; + + expect(await controller.palTokenToPalPool(fakeToken.address)).to.be.eq(fakePool.address) + expect(await controller.palTokenToPalPool(fakeToken2.address)).to.be.eq(ethers.constants.AddressZero) + expect(await controller.palTokenToPalPool(fakeToken3.address)).to.be.eq(fakePool3.address) }); it(' should not remove not-listed PalPool', async () => { @@ -175,8 +197,8 @@ describe('Paladin Controller contract tests', () => { await expect( controller.connect(admin).removePool(fakePool3.address), - 'Not listed' - ).to.be.revertedWith('Not listed') + '39' + ).to.be.revertedWith('39') }); it(' should not remove a PalPool twice', async () => { @@ -187,8 +209,8 @@ describe('Paladin Controller contract tests', () => { await controller.connect(admin).removePool(fakePool2.address) await expect( controller.connect(admin).removePool(fakePool2.address), - 'Not listed' - ).to.be.revertedWith('Not listed') + '39' + ).to.be.revertedWith('39') }); it(' should block non-admin to remove PalPool', async () => { @@ -239,36 +261,45 @@ describe('Paladin Controller contract tests', () => { await controller.connect(admin).borrowPossible(mockPool.address, invalidAmount) ).to.be.false + + await mockPool.testDepositVerify(fakePool.address, user1.address, 10) expect( - await controller.connect(fakePool.address).depositVerify(fakePool.address, user1.address, 10) + await mockPool.lastControllerCallResult() ).to.be.true + await mockPool.testWithdrawVerify(fakePool.address, user1.address, 10) expect( - await controller.connect(fakePool.address).withdrawVerify(fakePool.address, user1.address, 10) + await mockPool.lastControllerCallResult() ).to.be.true + await mockPool.testDepositVerify(fakePool.address, user1.address, 0) expect( - await controller.connect(fakePool.address).depositVerify(fakePool.address, user1.address, 0) + await mockPool.lastControllerCallResult() ).to.be.false + await mockPool.testWithdrawVerify(fakePool.address, user1.address, 0) expect( - await controller.connect(fakePool.address).withdrawVerify(fakePool.address, user1.address, 0) + await mockPool.lastControllerCallResult() ).to.be.false + await mockPool.testBorrowVerify(fakePool.address, user1.address, user1.address, 10, 5, fakeLoan.address) expect( - await controller.connect(fakePool.address).borrowVerify(fakePool.address, user1.address, user1.address, 10, 5, fakeLoan.address) + await mockPool.lastControllerCallResult() ).to.be.true + await mockPool.testExpandBorrowVerify(fakePool.address, fakeLoan.address, 5) expect( - await controller.connect(fakePool.address).expandBorrowVerify(fakePool.address, fakeLoan.address, 5) + await mockPool.lastControllerCallResult() ).to.be.true + await mockPool.testCloseBorrowVerify(fakePool.address, user1.address, fakeLoan.address) expect( - await controller.connect(fakePool.address).closeBorrowVerify(fakePool.address, user1.address, fakeLoan.address) + await mockPool.lastControllerCallResult() ).to.be.true + await mockPool.testKillBorrowVerify(fakePool.address, user1.address, fakeLoan.address) expect( - await controller.connect(fakePool.address).killBorrowVerify(fakePool.address, user1.address, fakeLoan.address) + await mockPool.lastControllerCallResult() ).to.be.true }); @@ -277,27 +308,27 @@ describe('Paladin Controller contract tests', () => { it(' should block PalPool functions to be called', async () => { await expect( controller.depositVerify(fakePool.address, user1.address, 10) - ).to.be.revertedWith('Call not allowed') + ).to.be.revertedWith('40') await expect( controller.withdrawVerify(fakePool.address, user1.address, 10) - ).to.be.revertedWith('Call not allowed') + ).to.be.revertedWith('40') await expect( controller.borrowVerify(fakePool.address, user1.address, user1.address, 10, 5, fakeLoan.address) - ).to.be.revertedWith('Call not allowed') + ).to.be.revertedWith('40') await expect( controller.expandBorrowVerify(fakePool.address, fakeLoan.address, 5) - ).to.be.revertedWith('Call not allowed') + ).to.be.revertedWith('40') await expect( controller.closeBorrowVerify(fakePool.address, user1.address, fakeLoan.address) - ).to.be.revertedWith('Call not allowed') + ).to.be.revertedWith('40') await expect( controller.killBorrowVerify(fakePool.address, user1.address, fakeLoan.address) - ).to.be.revertedWith('Call not allowed') + ).to.be.revertedWith('40') }); }); diff --git a/test/controller/proxyController.test.ts b/test/controller/proxyController.test.ts new file mode 100644 index 0000000..43cd869 --- /dev/null +++ b/test/controller/proxyController.test.ts @@ -0,0 +1,260 @@ +import { ethers, waffle } from "hardhat"; +import chai from "chai"; +import { solidity } from "ethereum-waffle"; +import { PaladinController } from "../../typechain/PaladinController"; +import { ControllerProxy } from "../../typechain/ControllerProxy"; +import { PaladinController__factory } from "../../typechain/factories/PaladinController__factory"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { ContractFactory } from "@ethersproject/contracts"; + +chai.use(solidity); +const { expect } = chai; +const { provider } = ethers; + + + +let controllerFactory: ContractFactory +let controllerProxyFactory: ContractFactory + + +describe('Controller Proxy contract tests', () => { + let admin: SignerWithAddress + let user: SignerWithAddress + let fakeImplementation: SignerWithAddress + + let fakePool: SignerWithAddress + let fakePool2: SignerWithAddress + let fakePool3: SignerWithAddress + let fakeToken: SignerWithAddress + let fakeToken2: SignerWithAddress + let fakeToken3: SignerWithAddress + + let controller: PaladinController + let proxy: ControllerProxy + let proxyWithImpl: PaladinController + let other_controller: PaladinController + + before( async () => { + controllerFactory = await ethers.getContractFactory("PaladinController"); + controllerProxyFactory = await ethers.getContractFactory("ControllerProxy"); + }) + + beforeEach( async () => { + [ + admin, + user, + fakePool, + fakePool2, + fakePool3, + fakeToken, + fakeToken2, + fakeToken3, + fakeImplementation + ] = await ethers.getSigners(); + + controller = (await controllerFactory.deploy()) as PaladinController; + await controller.deployed(); + + proxy = (await controllerProxyFactory.deploy()) as ControllerProxy; + await proxy.deployed(); + + }); + + + it(' should be deployed', async () => { + expect(controller.address).to.properAddress + expect(proxy.address).to.properAddress + }); + + + describe('Propose Implementation', async () => { + + it(' should set the correct address as pendingImplementation (& with correct Event)', async () => { + + await expect(proxy.connect(admin).proposeImplementation(controller.address)) + .to.emit(proxy, 'NewPendingImplementation') + .withArgs(ethers.constants.AddressZero, controller.address); + + expect(await proxy.pendingImplementation()).to.be.eq(controller.address) + + }); + + it(' should only be callable by admin', async () => { + + await expect( + proxy.connect(user).proposeImplementation(fakeImplementation.address) + ).to.be.revertedWith('1') + + }); + + }); + + + describe('Accept Implementation', async () => { + + beforeEach( async () => { + + other_controller = (await controllerFactory.deploy()) as PaladinController; + await other_controller.deployed(); + + await proxy.connect(admin).proposeImplementation(controller.address) + + }); + + it(' should be able to become Implementation (& with correct Events)', async () => { + + const become_tx = controller.connect(admin).becomeImplementation(proxy.address) + + await expect(become_tx) + .to.emit(proxy, 'NewImplementation') + .withArgs(ethers.constants.AddressZero, controller.address); + + await expect(become_tx) + .to.emit(proxy, 'NewPendingImplementation') + .withArgs(controller.address, ethers.constants.AddressZero); + + expect(await proxy.currentImplementation()).to.be.eq(controller.address) + expect(await proxy.pendingImplementation()).to.be.eq(ethers.constants.AddressZero) + + }); + + it(' should only accept pending implementations', async () => { + + await expect( + proxy.connect(fakeImplementation).acceptImplementation() + ).to.be.revertedWith('35') + + await expect( + other_controller.connect(admin).becomeImplementation(proxy.address) + ).to.be.revertedWith('35') + + }); + + it(' should only allow admin to call becomeImplementation', async () => { + + await expect( + controller.connect(user).becomeImplementation(proxy.address) + ).to.be.revertedWith('1') + + }); + + }); + + + describe('Act as a Proxy', async () => { + + beforeEach( async () => { + + await proxy.connect(admin).proposeImplementation(controller.address) + + await controller.connect(admin).becomeImplementation(proxy.address) + + proxyWithImpl = PaladinController__factory.connect(proxy.address, provider) as PaladinController + + }); + + it(' should be able to initialize using Controller method', async () => { + + const pools_list = [fakePool.address, fakePool2.address] + const tokens_list = [fakeToken.address, fakeToken2.address] + + await proxyWithImpl.connect(admin).setInitialPools(tokens_list, pools_list); + + const tokens = await proxyWithImpl.getPalTokens(); + const pools = await proxyWithImpl.getPalPools(); + + expect(pools).to.contain(fakePool.address) + expect(tokens).to.contain(fakeToken.address) + expect(pools).to.contain(fakePool2.address) + expect(tokens).to.contain(fakeToken2.address) + + expect(pools).not.to.contain(fakePool3.address) + expect(tokens).not.to.contain(fakeToken3.address) + + expect(await proxyWithImpl.isPalPool(fakePool.address)).to.be.true; + expect(await proxyWithImpl.isPalPool(fakePool2.address)).to.be.true; + expect(await proxyWithImpl.isPalPool(fakePool3.address)).to.be.false; + + await expect( + proxyWithImpl.connect(admin).setInitialPools([fakeToken.address], [fakePool.address]) + ).to.be.revertedWith('37') + + }); + + it(' should have correct Storage', async () => { + + const expected_initial_index = ethers.utils.parseUnits("1", 36); + + const initialIndex = await proxyWithImpl.initialRewardsIndex(); + + expect(initialIndex).to.be.eq(expected_initial_index) + + }); + + it(' should delegate function calls through fallback', async () => { + + const pools_list = [fakePool.address, fakePool2.address] + const tokens_list = [fakeToken.address, fakeToken2.address] + + await proxyWithImpl.connect(admin).setInitialPools(tokens_list, pools_list); + + await proxyWithImpl.connect(admin).addNewPool(fakeToken3.address, fakePool3.address) + + let tokens = await proxyWithImpl.getPalTokens(); + let pools = await proxyWithImpl.getPalPools(); + + expect(pools).to.contain(fakePool.address) + expect(tokens).to.contain(fakeToken.address) + expect(pools).to.contain(fakePool2.address) + expect(tokens).to.contain(fakeToken2.address) + expect(pools).to.contain(fakePool3.address) + expect(tokens).to.contain(fakeToken3.address) + + expect(await proxyWithImpl.isPalPool(fakePool.address)).to.be.true; + expect(await proxyWithImpl.isPalPool(fakePool2.address)).to.be.true; + expect(await proxyWithImpl.isPalPool(fakePool3.address)).to.be.true; + + await proxyWithImpl.connect(admin).removePool(fakePool2.address) + + tokens = await proxyWithImpl.getPalTokens(); + pools = await proxyWithImpl.getPalPools(); + + expect(pools).to.contain(fakePool.address) + expect(tokens).to.contain(fakeToken.address) + expect(pools).to.contain(fakePool3.address) + expect(tokens).to.contain(fakeToken3.address) + + expect(pools).not.to.contain(fakePool2.address) + expect(tokens).not.to.contain(fakeToken2.address) + + + expect(await proxyWithImpl.isPalPool(fakePool.address)).to.be.true; + expect(await proxyWithImpl.isPalPool(fakePool2.address)).to.be.false; + expect(await proxyWithImpl.isPalPool(fakePool3.address)).to.be.true; + + }); + + }); + + + describe('Drop Implementation', async () => { + + it(' should accept address 0x000...000 as pending implementation', async () => { + + await proxy.connect(admin).proposeImplementation(ethers.constants.AddressZero) + + expect(await proxy.pendingImplementation()).to.be.eq(ethers.constants.AddressZero) + + }); + + it(' should set address 0x000...000 as current implementation', async () => { + + await proxy.connect(admin).acceptImplementation() + + expect(await proxy.currentImplementation()).to.be.eq(ethers.constants.AddressZero) + + }); + + }); + +}); \ No newline at end of file diff --git a/test/controller/rewards.test.ts b/test/controller/rewards.test.ts new file mode 100644 index 0000000..add7541 --- /dev/null +++ b/test/controller/rewards.test.ts @@ -0,0 +1,1163 @@ +import { ethers, waffle } from "hardhat"; +import chai from "chai"; +import { solidity } from "ethereum-waffle"; +import { PaladinController } from "../../typechain/PaladinController"; +import { Comp } from "../../typechain/Comp"; +import { PalToken } from "../../typechain/PalToken"; +import { PalPool } from "../../typechain/PalPool"; +import { PalLoanToken } from "../../typechain/PalLoanToken"; +import { InterestCalculator } from "../../typechain/InterestCalculator"; +import { BasicDelegator } from "../../typechain/BasicDelegator"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { ContractFactory } from "@ethersproject/contracts"; + +chai.use(solidity); +const { expect } = chai; + +const mantissa = ethers.utils.parseEther('1') +const doubleMantissa = ethers.utils.parseEther('1000000000000000000') + +let controllerFactory: ContractFactory +let erc20Factory: ContractFactory + +let poolFactory: ContractFactory +let tokenFactory: ContractFactory +let delegatorFactory: ContractFactory +let interestFactory: ContractFactory +let palLoanTokenFactory: ContractFactory + + +describe('Paladin Controller - Rewards System tests', () => { + let admin: SignerWithAddress + let user1: SignerWithAddress + let user2: SignerWithAddress + let user3: SignerWithAddress + + let controller: PaladinController + + let pool1: PalPool + let token1: PalToken + let pool2: PalPool + let token2: PalToken + let loanToken: PalLoanToken + let delegator: BasicDelegator + let interest: InterestCalculator + let underlying: Comp + let rewardToken: Comp + + let fakeLoan: SignerWithAddress + + let name1: string = "Paladin MOCK 1" + let symbol1: string = "palMOCK1" + let name2: string = "Paladin MOCK 2" + let symbol2: string = "palMOCK2" + + let tx_setPools: any + + const mineBlocks = async (n: number): Promise => { + for(let i = 0; i < n; i++){ + await ethers.provider.send("evm_mine", []) + } + return Promise.resolve() + } + + before( async () => { + controllerFactory = await ethers.getContractFactory("PaladinController"); + erc20Factory = await ethers.getContractFactory("Comp"); + tokenFactory = await ethers.getContractFactory("PalToken"); + palLoanTokenFactory = await ethers.getContractFactory("PalLoanToken"); + poolFactory = await ethers.getContractFactory("PalPool"); + delegatorFactory = await ethers.getContractFactory("BasicDelegator"); + interestFactory = await ethers.getContractFactory("InterestCalculator"); + }) + + beforeEach( async () => { + [ + admin, + user1, + user2, + user3, + fakeLoan + ] = await ethers.getSigners(); + + controller = (await controllerFactory.deploy()) as PaladinController; + + underlying = (await erc20Factory.connect(admin).deploy(admin.address)) as Comp; + await underlying.deployed(); + + rewardToken = (await erc20Factory.connect(admin).deploy(admin.address)) as Comp; + await rewardToken.deployed(); + + //2 pools with 2 tokens + token1 = (await tokenFactory.connect(admin).deploy(name1, symbol1)) as PalToken; + await token1.deployed(); + + token2 = (await tokenFactory.connect(admin).deploy(name2, symbol2)) as PalToken; + await token2.deployed(); + + loanToken = (await palLoanTokenFactory.connect(admin).deploy(controller.address, "about:blank")) as PalLoanToken; + await loanToken.deployed(); + + interest = (await interestFactory.connect(admin).deploy()) as InterestCalculator; + await interest.deployed(); + + delegator = (await delegatorFactory.connect(admin).deploy()) as BasicDelegator; + await delegator.deployed(); + + pool1 = (await poolFactory.connect(admin).deploy( + token1.address, + controller.address, + underlying.address, + interest.address, + delegator.address, + loanToken.address + )) as PalPool; + await pool1.deployed(); + + await token1.initiate(pool1.address); + + pool2 = (await poolFactory.connect(admin).deploy( + token2.address, + controller.address, + underlying.address, + interest.address, + delegator.address, + loanToken.address + )) as PalPool; + await pool2.deployed(); + + await token2.initiate(pool2.address); + + const pools_list = [pool1.address, pool2.address] + const tokens_list = [token1.address, token2.address] + + tx_setPools = await controller.connect(admin).setInitialPools(tokens_list, pools_list); + + }); + + + it(' should be deployed', async () => { + expect(controller.address).to.properAddress + }); + + + describe('Rewards Values (Storage)', async () => { + + const supplySpeed = ethers.utils.parseEther("0.25") + const borrowRatio = ethers.utils.parseEther("0.75") + + it(' should have the correct base values in storage', async () => { + + const expected_initial_index = ethers.utils.parseUnits("1", 36); + + const reward_token_address = await controller.rewardToken(); + const initialIndex = await controller.initialRewardsIndex(); + + expect(reward_token_address).to.be.eq(ethers.constants.AddressZero) + expect(initialIndex).to.be.eq(expected_initial_index) + + + const setPools_block = (await tx_setPools).blockNumber || 0 + + const supplyRewardState1 = await controller.supplyRewardState(pool1.address) + const supplyRewardState2 = await controller.supplyRewardState(pool1.address) + + expect(await supplyRewardState1.index).to.be.eq(initialIndex) + expect(await supplyRewardState2.index).to.be.eq(initialIndex) + + expect(await supplyRewardState1.blockNumber).to.be.eq(setPools_block) + expect(await supplyRewardState2.blockNumber).to.be.eq(setPools_block) + + + expect(await controller.supplySpeeds(pool1.address)).to.be.eq(0) + expect(await controller.supplySpeeds(pool2.address)).to.be.eq(0) + + + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(0) + expect(await controller.supplierRewardIndex(pool2.address, user1.address)).to.be.eq(0) + + expect(await controller.supplierRewardIndex(pool1.address, user2.address)).to.be.eq(0) + expect(await controller.supplierRewardIndex(pool2.address, user2.address)).to.be.eq(0) + + expect(await controller.supplierRewardIndex(pool1.address, user3.address)).to.be.eq(0) + expect(await controller.supplierRewardIndex(pool2.address, user3.address)).to.be.eq(0) + + + expect(await controller.borrowRatios(pool1.address)).to.be.eq(0) + expect(await controller.borrowRatios(pool2.address)).to.be.eq(0) + + + expect(await controller.accruedRewards(user1.address)).to.be.eq(0) + expect(await controller.accruedRewards(user2.address)).to.be.eq(0) + expect(await controller.accruedRewards(user3.address)).to.be.eq(0) + + expect(await controller.claimable(user1.address)).to.be.eq(0) + expect(await controller.claimable(user2.address)).to.be.eq(0) + expect(await controller.claimable(user3.address)).to.be.eq(0) + + expect(await controller.borrowRewardsStartBlock(pool1.address)).to.be.eq(0) + expect(await controller.borrowRewardsStartBlock(pool2.address)).to.be.eq(0) + + }); + + it(' should allow admin to set rewards for PalPool', async () => { + + const updateSetting_tx = await controller.connect(admin).updatePoolRewards(pool1.address, supplySpeed, borrowRatio, true) + + await expect(updateSetting_tx) + .to.emit(controller, 'PoolRewardsUpdated') + .withArgs(pool1.address, supplySpeed, borrowRatio, true); + + expect(await controller.supplySpeeds(pool1.address)).to.be.eq(supplySpeed) + expect(await controller.borrowRatios(pool1.address)).to.be.eq(borrowRatio) + expect(await controller.autoBorrowRewards(pool1.address)).to.be.eq(true) + expect(await controller.borrowRewardsStartBlock(pool1.address)).to.be.eq((await updateSetting_tx).blockNumber) + + const new_supplySpeed = ethers.utils.parseEther("0.35") + const new_borrowRatio = ethers.utils.parseEther("0.55") + + await controller.connect(admin).updatePoolRewards(pool1.address, new_supplySpeed, borrowRatio, false); + + expect(await controller.supplySpeeds(pool1.address)).to.be.eq(new_supplySpeed) + expect(await controller.borrowRatios(pool1.address)).to.be.eq(borrowRatio) + expect(await controller.autoBorrowRewards(pool1.address)).to.be.eq(false) + expect(await controller.borrowRewardsStartBlock(pool1.address)).to.be.eq((await updateSetting_tx).blockNumber) + + await controller.connect(admin).updatePoolRewards(pool1.address, new_supplySpeed, new_borrowRatio, true); + + expect(await controller.supplySpeeds(pool1.address)).to.be.eq(new_supplySpeed) + expect(await controller.borrowRatios(pool1.address)).to.be.eq(new_borrowRatio) + expect(await controller.autoBorrowRewards(pool1.address)).to.be.eq(true) + expect(await controller.borrowRewardsStartBlock(pool1.address)).to.be.eq((await updateSetting_tx).blockNumber) + + }); + + it(' should allow admin to set reward token', async () => { + + const pal_contract_address = "0xAB846Fb6C81370327e784Ae7CbB6d6a6af6Ff4BF"; + + await controller.connect(admin).updateRewardToken(pal_contract_address) + + const reward_token_address = await controller.rewardToken(); + + expect(reward_token_address).to.be.eq(pal_contract_address) + + await expect( + controller.connect(user1).updateRewardToken(rewardToken.address) + ).to.be.revertedWith('1') + + }); + + it(' should not allow non-admin to set rewards for PalPool', async () => { + + await expect( + controller.connect(user1).updatePoolRewards(pool1.address, supplySpeed, borrowRatio, true) + ).to.be.revertedWith('1') + + }); + + it(' should not reset supplyRewardState.index if a previous one is not null', async () => { + + const amount = ethers.utils.parseEther('500') + await underlying.connect(admin).transfer(user1.address, amount) + await underlying.connect(user1).approve(pool1.address, amount) + await pool1.connect(user1).deposit(amount) + const user_palToken_amount = await token1.balanceOf(user1.address) + await token1.connect(user1).approve(controller.address, user_palToken_amount) + await controller.connect(user1).deposit(token1.address, user_palToken_amount) + + await controller.connect(admin).updatePoolRewards(pool1.address, supplySpeed, 0, true); + + await mineBlocks(75) + + await controller.connect(user1).updateUserRewards(user1.address) + + const supplyRewardState1 = await controller.supplyRewardState(pool1.address) + const initialIndex = await controller.initialRewardsIndex(); + expect(supplyRewardState1.index).not.to.be.eq(initialIndex) + + await controller.connect(admin).removePool(pool1.address) + await controller.connect(admin).addNewPool(token1.address, pool1.address) + + const supplyRewardState2 = await controller.supplyRewardState(pool1.address) + expect(supplyRewardState2.index).not.to.be.eq(initialIndex) + expect(supplyRewardState2.index).to.be.eq(supplyRewardState1.index) + + }); + + it(' should sum up supply speeds correctly', async () => { + + const supplySpeed1 = ethers.utils.parseEther("0.3") + const supplySpeed2 = ethers.utils.parseEther("0.55") + + await controller.connect(admin).updatePoolRewards(pool1.address, supplySpeed1, 0, true); + await controller.connect(admin).updatePoolRewards(pool2.address, supplySpeed2, 0, true); + + const controller_totalSpeed = await controller.totalSupplyRewardSpeed() + + expect(controller_totalSpeed).to.be.eq(supplySpeed1.add(supplySpeed2)) + + }); + + it(' should reset borrowRewardsStartBlock when Borrow Rewards Ratio is set back to 0', async () => { + + const updateSetting_tx = await controller.connect(admin).updatePoolRewards(pool1.address, supplySpeed, borrowRatio, false) + + expect(await controller.borrowRewardsStartBlock(pool1.address)).to.be.eq((await updateSetting_tx).blockNumber) + + await controller.connect(admin).updatePoolRewards(pool1.address, supplySpeed, 0, false) + + expect(await controller.borrowRewardsStartBlock(pool1.address)).to.be.eq(0) + + }); + + }); + + + describe('Supply Rewards', async () => { + + const supplySpeed = ethers.utils.parseEther("0.25") + + const reward_amount = ethers.utils.parseEther('100') + + const deposit_amount = ethers.utils.parseEther('500') + const deposit_amount2 = ethers.utils.parseEther('350') + + beforeEach( async () => { + await underlying.connect(admin).transfer(user1.address, deposit_amount) + await underlying.connect(user1).approve(pool1.address, deposit_amount) + + await pool1.connect(user1).deposit(deposit_amount) + + await controller.connect(admin).updatePoolRewards(pool1.address, supplySpeed, 0, true); + + await controller.connect(admin).updateRewardToken(rewardToken.address) + + await rewardToken.connect(admin).transfer(controller.address, reward_amount) + + }); + + it(' should have the correct Supply Speeds', async () => { + + expect(await controller.supplySpeeds(pool1.address)).to.be.eq(supplySpeed) + expect(await controller.supplySpeeds(pool2.address)).to.be.eq(0) + + }); + + it(' should allow to deposit correclty (& with correct Event)', async () => { + + const user_palToken_amount = await token1.balanceOf(user1.address) + + expect(await controller.supplierDeposits(pool1.address, user1.address)).to.be.eq(0) + expect(await controller.totalSupplierDeposits(pool1.address)).to.be.eq(0) + + await token1.connect(user1).approve(controller.address, user_palToken_amount) + + await expect(controller.connect(user1).deposit(token1.address, user_palToken_amount)) + .to.emit(controller, 'Deposit') + .withArgs(user1.address, token1.address, user_palToken_amount); + + expect(await controller.supplierDeposits(pool1.address, user1.address)).to.be.eq(user_palToken_amount) + expect(await controller.totalSupplierDeposits(pool1.address)).to.be.eq(user_palToken_amount) + + await underlying.connect(admin).transfer(user2.address, deposit_amount2) + await underlying.connect(user2).approve(pool1.address, deposit_amount2) + + await pool1.connect(user2).deposit(deposit_amount2) + + const user_palToken_amount2 = await token1.balanceOf(user2.address) + + await token1.connect(user2).approve(controller.address, user_palToken_amount2) + + await controller.connect(user2).deposit(token1.address, user_palToken_amount2) + + expect(await controller.supplierDeposits(pool1.address, user2.address)).to.be.eq(user_palToken_amount2) + expect(await controller.totalSupplierDeposits(pool1.address)).to.be.eq(user_palToken_amount.add(user_palToken_amount2)) + + + await token1.connect(user1).approve(controller.address, user_palToken_amount) + await expect( + controller.connect(user1).deposit(token1.address, user_palToken_amount) + ).to.be.revertedWith('10') + + }); + + it(' should allow to withdraw correctly (& with correct Event)', async () => { + + const user_palToken_amount = await token1.balanceOf(user1.address) + await token1.connect(user1).approve(controller.address, user_palToken_amount) + await controller.connect(user1).deposit(token1.address, user_palToken_amount) + + const to_withdraw_amount = user_palToken_amount.div(2) + + await expect(controller.connect(user1).withdraw(token1.address, to_withdraw_amount)) + .to.emit(controller, 'Withdraw') + .withArgs(user1.address, token1.address, to_withdraw_amount); + + expect(await controller.supplierDeposits(pool1.address, user1.address)).to.be.eq(user_palToken_amount.sub(to_withdraw_amount)) + expect(await controller.totalSupplierDeposits(pool1.address)).to.be.eq(user_palToken_amount.sub(to_withdraw_amount)) + + await expect( + controller.connect(user2).withdraw(token1.address, user_palToken_amount) + ).to.be.revertedWith('43') + + }); + + it(' should not accrue rewards if PalTokens are not deposited in the Controller', async () => { + + //user1 deposited in pool1 before, but not in the controller + + expect(await controller.claimable(user1.address)).to.be.eq(0) + + await mineBlocks(150) + + await controller.connect(user1).updateUserRewards(user1.address) + + expect(await controller.claimable(user1.address)).to.be.eq(0) + + }); + + it(' should accrue rewards to depositor (& allow to claim it) + set the correct SupplyRewardStates', async () => { + + const initial_index = await controller.initialRewardsIndex() + const user1_palToken_amount = await token1.balanceOf(user1.address) + + await token1.connect(user1).approve(controller.address, user1_palToken_amount) + + const deposit_tx = await controller.connect(user1).deposit(token1.address, user1_palToken_amount) + + const supplyRewardState1 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(supplyRewardState1.index) + expect(await controller.supplierRewardIndex(pool2.address, user1.address)).to.be.eq(0) + + const deposit_block = (await deposit_tx).blockNumber || 0 + expect(supplyRewardState1.blockNumber).to.be.eq(deposit_block) + + await mineBlocks(150) + + const update_tx = await controller.connect(user1).updateUserRewards(user1.address) + const update_block = (await update_tx).blockNumber || 0 + + const user_claimable = await controller.claimable(user1.address) + + const estimated_accrued_rewards = supplySpeed.mul(update_block - deposit_block) + + const new_supplyRewardState1 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(new_supplyRewardState1.index) + expect(await controller.supplierRewardIndex(pool2.address, user1.address)).to.be.eq(initial_index) + expect(new_supplyRewardState1.blockNumber).to.be.eq(update_block) + + expect(user_claimable).to.be.eq(estimated_accrued_rewards) + + const old_balance = await rewardToken.balanceOf(user1.address) + + const claim_tx = await controller.connect(user1).claim(user1.address) + const claim_block = (await claim_tx).blockNumber || 0 + + const new_balance = await rewardToken.balanceOf(user1.address) + + const estimated_accrued_rewards_2 = supplySpeed.mul(claim_block - update_block) + + expect(await controller.claimable(user1.address)).to.be.eq(0) + expect(new_balance.sub(old_balance)).to.be.eq(estimated_accrued_rewards.add(estimated_accrued_rewards_2)) + + }); + + it(' should accrue rewards with multiple depositors in the Pool', async () => { + + await underlying.connect(admin).transfer(user2.address, deposit_amount2) + await underlying.connect(user2).approve(pool1.address, deposit_amount2) + await pool1.connect(user2).deposit(deposit_amount2) + + const user1_palToken_amount = await token1.balanceOf(user1.address) + const user2_palToken_amount = await token1.balanceOf(user2.address) + + await token1.connect(user1).approve(controller.address, user1_palToken_amount) + await token1.connect(user2).approve(controller.address, user2_palToken_amount) + + + const deposit_tx1 = await controller.connect(user1).deposit(token1.address, user1_palToken_amount) + + const supplyRewardState1 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(supplyRewardState1.index) + const deposit_block1 = (await deposit_tx1).blockNumber || 0 + expect(supplyRewardState1.blockNumber).to.be.eq(deposit_block1) + + + const deposit_tx2 = await controller.connect(user2).deposit(token1.address, user2_palToken_amount) + + const supplyRewardState2 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user2.address)).to.be.eq(supplyRewardState2.index) + const deposit_block2 = (await deposit_tx2).blockNumber || 0 + expect(supplyRewardState2.blockNumber).to.be.eq(deposit_block2) + + + await mineBlocks(123) + + + const totalDeposited = await controller.totalSupplierDeposits(pool1.address) + + + const update_tx1 = await controller.connect(user1).updateUserRewards(user1.address) + const update_block1 = (await update_tx1).blockNumber || 0 + + const user1_claimable = await controller.claimable(user1.address) + + const estimated_accrued_rewards_user1 = supplySpeed.mul(deposit_block2 - deposit_block1).add( + supplySpeed.mul(update_block1 - deposit_block2).mul( + user1_palToken_amount.mul(doubleMantissa).div(totalDeposited) + ).div(doubleMantissa) + ) + + + const update_tx2 = await controller.connect(user2).updateUserRewards(user2.address) + const update_block2 = (await update_tx2).blockNumber || 0 + + const user2_claimable = await controller.claimable(user2.address) + + const estimated_accrued_rewards_user2 = supplySpeed.mul(update_block2 - deposit_block2).mul( + user2_palToken_amount.mul(doubleMantissa).div(totalDeposited) + ).div(doubleMantissa) + + const new_supplyRewardState1 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user2.address)).to.be.eq(new_supplyRewardState1.index) + expect(new_supplyRewardState1.blockNumber).to.be.eq(update_block2) + + expect(user1_claimable).to.be.closeTo(estimated_accrued_rewards_user1, 10) + expect(user2_claimable).to.be.closeTo(estimated_accrued_rewards_user2, 10) + + + const old_balance1 = await rewardToken.balanceOf(user1.address) + + const claim1_tx = await controller.connect(user1).claim(user1.address) + const claim1_block = (await claim1_tx).blockNumber || 0 + + const new_balance1 = await rewardToken.balanceOf(user1.address) + + const estimated_accrued_rewards_user1_2 = supplySpeed.mul(claim1_block - update_block1).mul( + user1_palToken_amount.mul(doubleMantissa).div(totalDeposited) + ).div(doubleMantissa) + + expect(await controller.claimable(user1.address)).to.be.eq(0) + expect(new_balance1.sub(old_balance1)).to.be.closeTo(estimated_accrued_rewards_user1.add(estimated_accrued_rewards_user1_2), 10) + + + const old_balance2 = await rewardToken.balanceOf(user2.address) + + const claim2_tx = await controller.connect(user2).claim(user2.address) + const claim2_block = (await claim2_tx).blockNumber || 0 + + const new_balance2 = await rewardToken.balanceOf(user2.address) + + const estimated_accrued_rewards_user2_2 = supplySpeed.mul(claim1_block - update_block2).mul( + user2_palToken_amount.mul(doubleMantissa).div(totalDeposited) + ).div(doubleMantissa).add( + supplySpeed.mul(claim2_block - claim1_block).mul( + user2_palToken_amount.mul(doubleMantissa).div(totalDeposited) + ).div(doubleMantissa) + ) + + expect(await controller.claimable(user2.address)).to.be.eq(0) + expect(new_balance2.sub(old_balance2)).to.be.closeTo(estimated_accrued_rewards_user2.add(estimated_accrued_rewards_user2_2), 10) + + }); + + it(' should not accrue rewards if no speed was set + set the correct SupplyRewardStates', async () => { + + await underlying.connect(admin).transfer(user2.address, deposit_amount) + await underlying.connect(user2).approve(pool2.address, deposit_amount) + + await pool2.connect(user2).deposit(deposit_amount) + + const user2_palToken_amount = await token2.balanceOf(user2.address) + + await token2.connect(user2).approve(controller.address, user2_palToken_amount) + + await controller.connect(user2).deposit(token2.address, user2_palToken_amount) + + await mineBlocks(180) + + expect(await controller.claimable(user2.address)).to.be.eq(0) + + }); + + it(' should accrue the reward from 2 pools at the same time + set the correct SupplyRewardStates', async () => { + + await underlying.connect(admin).transfer(user1.address, deposit_amount) + await underlying.connect(user1).approve(pool2.address, deposit_amount) + await pool2.connect(user1).deposit(deposit_amount) + + const supplySpeed2 = ethers.utils.parseEther("0.45") + + await controller.connect(admin).updatePoolRewards(pool2.address, supplySpeed2, 0, true); + + const user1_palToken1_amount = await token1.balanceOf(user1.address) + const user1_palToken2_amount = await token2.balanceOf(user1.address) + + await token1.connect(user1).approve(controller.address, user1_palToken1_amount) + await token2.connect(user1).approve(controller.address, user1_palToken2_amount) + + const deposit1_tx = await controller.connect(user1).deposit(token1.address, user1_palToken1_amount) + const deposit2_tx = await controller.connect(user1).deposit(token2.address, user1_palToken2_amount) + + const supplyRewardState1 = await controller.supplyRewardState(pool1.address) + const supplyRewardState2 = await controller.supplyRewardState(pool2.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(supplyRewardState1.index) + expect(await controller.supplierRewardIndex(pool2.address, user1.address)).to.be.eq(supplyRewardState2.index) + + const deposit1_block = (await deposit1_tx).blockNumber || 0 + const deposit2_block = (await deposit2_tx).blockNumber || 0 + expect(supplyRewardState1.blockNumber).to.be.eq(deposit1_block) + expect(supplyRewardState2.blockNumber).to.be.eq(deposit2_block) + + await mineBlocks(180) + + const update_tx = await controller.connect(user1).updateUserRewards(user1.address) + const update_block = (await update_tx).blockNumber || 0 + + const user_claimable = await controller.claimable(user1.address) + + const estimated_accrued_rewards1 = supplySpeed.mul(update_block - deposit1_block) + const estimated_accrued_rewards2 = supplySpeed2.mul(update_block - deposit2_block) + + const new_supplyRewardState1 = await controller.supplyRewardState(pool1.address) + const new_supplyRewardState2 = await controller.supplyRewardState(pool2.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(new_supplyRewardState1.index) + expect(await controller.supplierRewardIndex(pool2.address, user1.address)).to.be.eq(new_supplyRewardState2.index) + expect(new_supplyRewardState1.blockNumber).to.be.eq(update_block) + expect(new_supplyRewardState2.blockNumber).to.be.eq(update_block) + + expect(user_claimable).to.be.eq(estimated_accrued_rewards1.add(estimated_accrued_rewards2)) + + }); + + it(' should show good estimateClaimable if Pool SupplyRewardState was updated but user rewards were not accrued', async () => { + + await underlying.connect(admin).transfer(user2.address, deposit_amount) + await underlying.connect(user2).approve(pool1.address, deposit_amount) + await pool1.connect(user2).deposit(deposit_amount) + + const user1_palToken_amount = await token1.balanceOf(user1.address) + + await token1.connect(user1).approve(controller.address, user1_palToken_amount) + + const user2_palToken_amount = await token1.balanceOf(user2.address) + + await token1.connect(user2).approve(controller.address, user2_palToken_amount) + + + + const deposit_tx = await controller.connect(user1).deposit(token1.address, user1_palToken_amount) + + const supplyRewardState1 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(supplyRewardState1.index) + + const deposit_block = (await deposit_tx).blockNumber || 0 + expect(supplyRewardState1.blockNumber).to.be.eq(deposit_block) + + await mineBlocks(150) + + const update_tx = await controller.connect(user2).deposit(token1.address, user2_palToken_amount) //use this method with the same values to update + const update_block = (await update_tx).blockNumber || 0 //the Pool Supply Reward State + + const user_estimateClaimable = await controller.estimateClaimable(user1.address) + + const estimated_accrued_rewards = supplySpeed.mul(update_block - deposit_block) + + const new_supplyRewardState1 = await controller.supplyRewardState(pool1.address) + expect(await controller.supplierRewardIndex(pool1.address, user1.address)).to.be.eq(supplyRewardState1.index) + expect(new_supplyRewardState1.blockNumber).to.be.eq(update_block) + + expect(user_estimateClaimable).to.be.eq(estimated_accrued_rewards) + + //Since user2 deposit was the last update, its estimation should be at 0 + expect(await controller.estimateClaimable(user2.address)).to.be.eq(0) + + }); + + it(' should do a correct update when Supply Speed is updated', async () => { + + const new_supplySpeed = ethers.utils.parseEther("0.45") + + const update_tx = await controller.connect(admin).updatePoolRewards(pool1.address, new_supplySpeed, 0, true); + + const update_block = (await update_tx).blockNumber || 0 + + const supplyRewardState = await controller.supplyRewardState(pool1.address) + expect(supplyRewardState.blockNumber).to.be.eq(update_block) + + }); + + it(' should stop accruing rewards if the Pool speed is set back to 0', async () => { + + const user1_palToken_amount = await token1.balanceOf(user1.address) + + await token1.connect(user1).approve(controller.address, user1_palToken_amount) + + await controller.connect(user1).deposit(token1.address, user1_palToken_amount) + + await mineBlocks(150) + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, 0, true); + + const old_claimable_amount = await controller.claimable(user1.address) + + await mineBlocks(200) + + const new_claimable_amount = await controller.claimable(user1.address) + + expect(new_claimable_amount).to.be.eq(old_claimable_amount) + + }); + + it(' should stop accruing rewards if depositor withdraw palTokens', async () => { + + const user1_palToken_amount = await token1.balanceOf(user1.address) + + await token1.connect(user1).approve(controller.address, user1_palToken_amount) + + await controller.connect(user1).deposit(token1.address, user1_palToken_amount) + + await mineBlocks(150) + + await controller.connect(user1).withdraw(token1.address, user1_palToken_amount) + + const old_claimable_amount = await controller.claimable(user1.address) + + await mineBlocks(200) + + const new_claimable_amount = await controller.claimable(user1.address) + + expect(new_claimable_amount).to.be.eq(old_claimable_amount) + + }); + + }); + + + describe('Borrow Rewards - auto accruing', async () => { + + const borrowRatio = ethers.utils.parseEther("0.75") + + const reward_amount = ethers.utils.parseEther('100') + + const deposit_amount = ethers.utils.parseEther('500') + const borrow_amount = ethers.utils.parseEther('35') + const borrow_fees = ethers.utils.parseEther('5') + const expand_fees = ethers.utils.parseEther('1') + + beforeEach( async () => { + await underlying.connect(admin).transfer(user1.address, deposit_amount) + await underlying.connect(user1).approve(pool1.address, deposit_amount) + await pool1.connect(user1).deposit(deposit_amount) + + await underlying.connect(admin).transfer(user2.address, borrow_fees.add(expand_fees)) + await underlying.connect(user2).approve(pool1.address, borrow_fees.add(expand_fees)) + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, borrowRatio, true); + + await controller.connect(admin).updateRewardToken(rewardToken.address) + + await rewardToken.connect(admin).transfer(controller.address, reward_amount) + + }); + + it(' should set the right ratio when Borrow starts', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + const loan_ratio = await controller.loansBorrowRatios(loan_address) + + expect(loan_ratio).to.be.eq(borrowRatio) + + }); + + it(' should accrue the right amount amount of rewards based on Loan ratio (& be claimable)', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + const loan_data = await pool1.getBorrowData(loan_address) + + const expected_rewards = (loan_data._feesUsed).mul(borrowRatio).div(mantissa) + + const user_claimable_rewards = await controller.claimable(user2.address) + + expect(expected_rewards).to.be.eq(user_claimable_rewards) + + const old_balance = await rewardToken.balanceOf(user2.address) + + await expect(controller.connect(user2).claim(user2.address)) + .to.emit(controller, 'ClaimRewards') + .withArgs(user2.address, expected_rewards); + + const new_balance = await rewardToken.balanceOf(user2.address) + + expect(new_balance.sub(old_balance)).to.be.eq(user_claimable_rewards) + + expect(await controller.isLoanRewardClaimed(loan_address)).to.be.true + + }); + + it(' should still use initial Loan ratio if BorrowRatio is changed', async () => { + + const new_BorrowRatio = ethers.utils.parseEther("0.55") + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + const loan_ratio = await controller.loansBorrowRatios(loan_address) + + expect(loan_ratio).to.be.eq(borrowRatio) + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, new_BorrowRatio, true); + + await pool1.connect(user2).closeBorrow(loan_address) + + const loan_data = await pool1.getBorrowData(loan_address) + + const expected_rewards = (loan_data._feesUsed).mul(loan_ratio).div(mantissa) + + const user_claimable_rewards = await controller.claimable(user2.address) + + expect(expected_rewards).to.be.eq(user_claimable_rewards) + + }); + + it(' should update the ratio if Loan is Expanded', async () => { + + const new_BorrowRatio = ethers.utils.parseEther("0.55") + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, new_BorrowRatio, true); + + await pool1.connect(user2).expandBorrow(loan_address, expand_fees) + + const loan_ratio = await controller.loansBorrowRatios(loan_address) + + expect(loan_ratio).to.be.eq(new_BorrowRatio) + + }); + + it(' should not allow to claim if not enough Reward Tokens in the Controller', async () => { + + const bigger_deposit_amount = ethers.utils.parseEther('5000000') + const bigger_borrow_amount = ethers.utils.parseEther('35000') + const bigger_borrow_fees = ethers.utils.parseEther('1500') + + await underlying.connect(admin).transfer(user1.address, bigger_deposit_amount) + await underlying.connect(user1).approve(pool1.address, bigger_deposit_amount) + await pool1.connect(user1).deposit(bigger_deposit_amount) + + await underlying.connect(admin).transfer(user2.address, bigger_borrow_fees) + await underlying.connect(user2).approve(pool1.address, bigger_borrow_fees) + + + await pool1.connect(user2).borrow(user2.address, bigger_borrow_amount, bigger_borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + await expect( + controller.connect(user2).claim(user2.address) + ).to.be.revertedWith('41') + + }); + + it(' should not give rewards if no BorrowRatio set for the Pool', async () => { + + await underlying.connect(admin).transfer(user1.address, deposit_amount) + await underlying.connect(user1).approve(pool2.address, deposit_amount) + await pool2.connect(user1).deposit(deposit_amount) + + await underlying.connect(admin).transfer(user2.address, borrow_fees) + await underlying.connect(user2).approve(pool2.address, borrow_fees) + + + await pool2.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + + const loan_address = (await pool2.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool2.connect(user2).closeBorrow(loan_address) + + const user_claimable_rewards = await controller.claimable(user2.address) + + expect(user_claimable_rewards).to.be.eq(0) + + }); + + it(' should not allow claiming through claimLoanRewards', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + expect(await controller.claimableLoanRewards(pool1.address, loan_address)).to.be.eq(0) + + await expect( + controller.connect(user2).claimLoanRewards(pool1.address, loan_address) + ).to.be.revertedWith('44') + + }); + + }); + + describe('Borrow Rewards - non-auto accruing', async () => { + + const borrowRatio = ethers.utils.parseEther("0.75") + + const reward_amount = ethers.utils.parseEther('100') + + const deposit_amount = ethers.utils.parseEther('500') + const borrow_amount = ethers.utils.parseEther('35') + const borrow_fees = ethers.utils.parseEther('5') + + beforeEach( async () => { + await underlying.connect(admin).transfer(user1.address, deposit_amount) + await underlying.connect(user1).approve(pool1.address, deposit_amount) + await pool1.connect(user1).deposit(deposit_amount) + + await underlying.connect(admin).transfer(user2.address, borrow_fees) + await underlying.connect(user2).approve(pool1.address, borrow_fees) + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, borrowRatio, false); + + await controller.connect(admin).updateRewardToken(rewardToken.address) + + await rewardToken.connect(admin).transfer(controller.address, reward_amount) + + }); + + it(' should not set a LoanRatio when Borrowing', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + const loan_ratio = await controller.loansBorrowRatios(loan_address) + + expect(loan_ratio).to.be.eq(0) + + }); + + it(' should not accrue rewards when Closing the Loan', async () => { + + const old_user_claimable_rewards = await controller.claimable(user2.address) + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + const new_user_claimable_rewards = await controller.claimable(user2.address) + + expect(new_user_claimable_rewards).to.be.eq(old_user_claimable_rewards) + + expect(await controller.isLoanRewardClaimed(loan_address)).to.be.false + + }); + + it(' should not allow to claim rewards on an active Loan', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + expect(await controller.claimableLoanRewards(pool1.address, loan_address)).to.be.eq(0) + + await expect( + controller.connect(user2).claimLoanRewards(pool1.address, loan_address) + ).to.be.revertedWith('44') + + }); + + it(' should have the correct amount of claimable rewards when Loan is closed', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + const loan_data = await pool1.getBorrowData(loan_address) + + const expected_rewards = (loan_data._feesUsed).mul(borrowRatio).div(mantissa) + + expect(await controller.claimableLoanRewards(pool1.address, loan_address)).to.be.eq(expected_rewards) + + }); + + it(' should allow to claim the rewards when Loan is closed', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + const claimable_rewards = await controller.claimableLoanRewards(pool1.address, loan_address) + + const old_balance = await rewardToken.balanceOf(user2.address) + + await expect(controller.connect(user2).claimLoanRewards(pool1.address, loan_address)) + .to.emit(controller, 'ClaimRewards') + .withArgs(user2.address, claimable_rewards); + + const new_balance = await rewardToken.balanceOf(user2.address) + + expect(new_balance.sub(old_balance)).to.be.eq(claimable_rewards) + + expect(await controller.isLoanRewardClaimed(loan_address)).to.be.true + + }); + + it(' should not allow to claim the rewards twice', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + await controller.connect(user2).claimLoanRewards(pool1.address, loan_address) + + await expect( + controller.connect(user2).claimLoanRewards(pool1.address, loan_address) + ).to.be.revertedWith('44') + + }); + + it(' should only allow the borrower to claim the rewards', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + await expect( + controller.connect(user1).claimLoanRewards(pool1.address, loan_address) + ).to.be.revertedWith('15') + + }); + + it(' should always use the current Pool Borrow Ratio', async () => { + + const new_borrowRatio = ethers.utils.parseEther("0.75") + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, new_borrowRatio, false); + + await pool1.connect(user2).closeBorrow(loan_address) + + const loan_data = await pool1.getBorrowData(loan_address) + + const expected_rewards = (loan_data._feesUsed).mul(new_borrowRatio).div(mantissa) + + expect(await controller.claimableLoanRewards(pool1.address, loan_address)).to.be.eq(expected_rewards) + + }); + + it(' should allow to claim is rewards are set as non-auto before Loan is closed (& use the Loan ratio)', async () => { + + const new_borrowRatio = ethers.utils.parseEther("0.75") + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, borrowRatio, true); + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await controller.connect(admin).updatePoolRewards(pool1.address, 0, new_borrowRatio, false); + + const loan_borrowRatio = await controller.loansBorrowRatios(loan_address) + + await pool1.connect(user2).closeBorrow(loan_address) + + const loan_data = await pool1.getBorrowData(loan_address) + + const expected_rewards = (loan_data._feesUsed).mul(loan_borrowRatio).div(mantissa) + + expect(loan_borrowRatio).to.be.eq(borrowRatio) + + expect(await controller.claimableLoanRewards(pool1.address, loan_address)).to.be.eq(expected_rewards) + + const old_balance = await rewardToken.balanceOf(user2.address) + + await expect(controller.connect(user2).claimLoanRewards(pool1.address, loan_address)) + .to.emit(controller, 'ClaimRewards') + .withArgs(user2.address, expected_rewards); + + const new_balance = await rewardToken.balanceOf(user2.address) + + expect(new_balance.sub(old_balance)).to.be.eq(expected_rewards) + + expect(await controller.isLoanRewardClaimed(loan_address)).to.be.true + + }); + + it(' should not give rewards to a PalLoan taken before rewards were set', async () => { + + await underlying.connect(admin).transfer(user1.address, deposit_amount) + await underlying.connect(user1).approve(pool2.address, deposit_amount) + await pool2.connect(user1).deposit(deposit_amount) + + await underlying.connect(admin).transfer(user2.address, borrow_fees) + await underlying.connect(user2).approve(pool2.address, borrow_fees) + + await pool2.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool2.getLoansByBorrower(user2.address)).slice(-1)[0] + + await controller.connect(admin).updatePoolRewards(pool2.address, 0, borrowRatio, false); + + await pool2.connect(user2).closeBorrow(loan_address) + + await expect( + controller.connect(user2).claimLoanRewards(pool2.address, loan_address) + ).to.be.revertedWith('44') + + }); + + it(' should fail if not correct PalPool for PalLoan', async () => { + + await pool1.connect(user2).borrow(user2.address, borrow_amount, borrow_fees) + + const loan_address = (await pool1.getLoansByBorrower(user2.address)).slice(-1)[0] + + await pool1.connect(user2).closeBorrow(loan_address) + + await expect( + controller.connect(user2).claimLoanRewards(pool2.address, loan_address) + ).to.be.reverted + + }); + + it(' should fail if not a PalLoan', async () => { + + await expect( + controller.connect(user2).claimLoanRewards(pool1.address, fakeLoan.address) + ).to.be.reverted + + }); + + }); + +}); \ No newline at end of file