Skip to content
This repository has been archived by the owner on May 22, 2023. It is now read-only.

Simple liquidity rewards #645

Merged
merged 41 commits into from
Dec 21, 2020
Merged

Simple liquidity rewards #645

merged 41 commits into from
Dec 21, 2020

Conversation

mhluongo
Copy link
Member

@mhluongo mhluongo commented Dec 14, 2020

Parameterized Synthetix's Unipool implementation for more flexible reward delivery.

@dimpar I'll let you take the PR from here — I haven't tried to build or thought about deploys, but the change to Unipool is simple. I'm also confident in the original source that I pulled in from mainnet.

Initially we need LPRewards deployed for these pairs

TBTC/ETH - https://info.uniswap.org/pair/0x854056fd40c1b52037166285b2e54fee774d33f6
KEEP/ETH - https://info.uniswap.org/pair/0xe6f19dab7d43317344282f803f8e8d240708174a
KEEP/TBTC - https://info.uniswap.org/pair/0x38c8ffee49f286f25d25bad919ff7552e5daf081

Copy link
Member

@nkuba nkuba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a first part of comments. I hope we will add more tests to cover all the functions in the contracts.

solidity/contracts/LPRewards.sol Outdated Show resolved Hide resolved
solidity/contracts/LPRewards.sol Show resolved Hide resolved
solidity/contracts/LPRewards.sol Show resolved Hide resolved
solidity/contracts/LPRewards.sol Show resolved Hide resolved
solidity/contracts/LPRewards.sol Show resolved Hide resolved
solidity/contracts/LPRewards.sol Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
@dimpar
Copy link
Contributor

dimpar commented Dec 16, 2020

Leaving a first part of comments. I hope we will add more tests to cover all the functions in the contracts.

Yes, I was planning on adding some more tests for our purposes to mainly show the different flows, but please note that Unipool.sol was already tested here. I take that this is a trusted source. These contracts are heavily based on Unipool.sol with some minor changes.

@mhluongo
Copy link
Member Author

Indeed, this has been audited and in highly-visible production for the past 8 months. We just need confidence on the diff

@dimpar
Copy link
Contributor

dimpar commented Dec 17, 2020

I compared the imported libs to make sure we're not breaking anything under the hood by using OpenZeppelin v2.3.0 instead of Unipool's OpenZeppeling v2.4.0. Here is OZ Changelog for v2.4.0. I don't see anything in "Breaking changes" that can affect us and it looks like we can use imports from v2.3.0.

I compared the initial commit df98c9b with OZ v2.3.0 and here's my findings:

  • Math.sol - same
  • SafeMath.sol - updated to throw custom errors on subtraction overflows.
  • Context.sol - instead of using _msgSender() which returns msg.sender now we just return msg.sender
  • Ownable.sol - same, it used '_msgSender()' from Context.sol which just returned msg.sender
  • IERC20.sol - same, changes were only in the comments
  • Address.sol - is used by SafeERC20. We are interested in one function Address.isContract(). This function changed in v2.4.0 to lower the gas consumption, HOWEVER later they figured out that it is not cheaper and switched the code back. Master has exactly the same code as v2.3.0 (which what we currently use). I think we are good on this front.
  • SafeERC20.sol - minor change in error message when subtracting.

In contracts we use "wrapped" token but in tests the same token is named
"lpToken". To reference the same token, this commit changes "lp" to
"wrapped" for ERC-20 pair token.
solidity/contracts/LPRewards.sol Outdated Show resolved Hide resolved
solidity/contracts/LPRewards.sol Show resolved Hide resolved
solidity/test/LPRewardsTest.js Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
})

describe("tokens allocation", () => {
it("should successfully allocate KEEP tokens", async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("should successfully allocate KEEP tokens", async () => {
it("should successfully allocate KEEP tokens via receiveApproval function", async () => {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if it's an allocation we are testing here. receiveApproval is just transferring tokens. The real allocation is receiveApproval + notifyRewardsAmount. Thoughts on renaming test scenario?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, maybe even better, we can add notifyRewardAmount call to this scenario. I do not think there is a scenario where we call receiveApproval without calling notifyRewardsAmount.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, heh. receiveApproval needs to call notifyRewardsAmount because PhasedEscrow's beneficiary will be the reward allocation contract.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b148edc Now we transfer funds in notifyRewardsAmount() and receiveApproval is gone

Comment on lines 28 to 37
keepToken = await KeepToken.new()
// This is a "Pair" Uniswap Token which is created here:
// https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Factory.sol#L23
//
// There are 3 addresses for the following pairs:
// - KEEP/ETH (https://info.uniswap.org/pair/0xe6f19dab7d43317344282f803f8e8d240708174a)
// - TBTC/ETH (https://info.uniswap.org/pair/0x854056fd40c1b52037166285b2e54fee774d33f6)
// - KEEP/TBTC (https://info.uniswap.org/pair/0x38c8ffee49f286f25d25bad919ff7552e5daf081)
wrappedToken = await WrappedToken.new()
lpRewards = await LPRewards.new(keepToken.address, wrappedToken.address)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add new variables keepTokenContractOwner, wrappedTokenContractOwner and lpRewardsContractOwner that would be different account than default 0 and deploy contracts from these accounts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solidity/contracts/LPRewards.sol Outdated Show resolved Hide resolved
solidity/contracts/LPRewards.sol Outdated Show resolved Hide resolved
solidity/contracts/LPRewards.sol Show resolved Hide resolved
expect(keepBalance).to.eq.BN(rewards)
})

it("should successfully allocate wrapped tokens", async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we organize test cases per function? e.g.LPTokens -> stake -> should successfully allocate wrapped tokens?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this test be named ... stake wrapped tokens? We are testing stake function here.

Copy link
Contributor

@dimpar dimpar Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Piotr: dc0f47c

KEEP token is the only token that we reward people for participating in
the keep network.
const wrappedTokenWalletBalance = await wrappedToken.balanceOf(wallet1)
expect(wrappedTokenWalletBalance).to.eq.BN(wrappedTokenWalletBallance)
})
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need a test for getReward function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exit() function above in this test calls getReward() and withdraw(). Then we check if a user actually received the rewards by calling getReward()

solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
expect(keepBalance).to.eq.BN(rewards)
})

it("should successfully allocate wrapped tokens", async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this test be named ... stake wrapped tokens? We are testing stake function here.

from: staker2,
})

wrappedTokenBalance = await wrappedToken.balanceOf(lpRewards.address)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also check the state of lpRewards:

  • lpRewards.totalSupply()
  • lpRewards.balanceOf(staker1)
  • lpRewards.balanceOf(staker2)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 137 to 138
const future = (await time.latest()).add(time.duration.days(7))
await timeIncreaseTo(future)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use periodFinish for that?

Suggested change
const future = (await time.latest()).add(time.duration.days(7))
await timeIncreaseTo(future)
await timeIncreaseTo(await lpRewards.periodFinish.call())

Copy link
Contributor

@dimpar dimpar Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about that. Having a separate duration of 7 days in test also checks duration in the contract.

const future = (await time.latest()).add(time.duration.days(7))
await timeIncreaseTo(future)

const actualEarnings = (await lpRewards.earned(staker1)).div(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of doing .div(tokenDecimalMultiplier) and .mul(tokenDecimalMultiplier) in so many places. Could we define values at the begining on the test including tokenDecimalMultiplier and don't use it aferwards?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it doesn't look pretty, but I think there is a value when testing with tokenDecimalMultiplier which reflects the scenario when working with real tokens. When transferring tokens to a contract and then retrieving them, we need to divide by 10**18 which will also show a precision error that we're dealing with. This is a similar approach in other tests for rewards, ex. TestECDSARewards.js.
How about instead of multiplication we would use web3.utils.toWei() function 4e8d014?

Comment on lines +155 to +160
expect(actualRewardPerToken).to.gte.BN(
expectedRewardPerWrappedToken.subn(precision)
)
expect(actualRewardPerToken).to.lte.BN(
expectedRewardPerWrappedToken.addn(precision)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woud could try to calculate the exact amount like in the example below. But also we can hardcode the expected value.

      const duration = await lpRewards.DURATION.call()
      const rewardRate = keepRewards.div(duration)

      const expectedRewardPerWrappedToken = rewardRate
        .mul(duration)
        .mul(tokenDecimalMultiplier)
        .div(stakedAmount)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding can be the cleaniest solution. Of course we need to add a comment next to the value definition with steps how it was calculated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a comment showing the math behind a reward per token. b0165bd

I think checking math on contract with a simple expected value in test should work, especially this is considered to be a trusted source.

Comment on lines +196 to +197
expect(keepEarnedRewards).to.gte.BN(rewardsAmount.subn(precision))
expect(keepEarnedRewards).to.lte.BN(rewardsAmount.addn(precision))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can hardcode expectedEarnedRewards so we can have just one exact equality check.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would that help? It's a time precision issue to my understanding.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precision error is very small, we had the same issue with TestECDSARewards.js and handled it the same way there. Besides, looks like Unipool.js also tests values from Solidy in a similar fashion by using almost equal approach.

r-czajkowski added a commit to keep-network/keep-core that referenced this pull request Dec 18, 2020
We need 3 dedicated LPRewards contracts- each for Uniswap pair KEEP/ETH,
TBTC/ETH, KEEP/TBTC. We are going to store a separate artifact for each
Uniswap pair, so we should replace paths to artifacts in a followup
work. Ref: keep-network/keep-ecdsa#645
Instead of adding approveAndCall function, it is cleaner to add a
'transfer' function in notifyRewardAmount which has to be called anyway
by the reward distributor. Reward distributor in our case is escrow's
beneficiary which is going to be set by PhasedEscrow contract.

In tests, the role of PhasedEscrow beneficiary takes rewardDistribution
account. We need to transfer funds from KEEP token owner to rewardDistribution
address first, before we can call notifyRewardAmount().
@pdyraga
Copy link
Member

pdyraga commented Dec 21, 2020

@dimpar Can you update this branch with master please?

Copy link
Member

@pdyraga pdyraga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Five comments to tests, I consider neither of them blocking. Need some time to go through the contract code one more time. @dimpar if you can address them in the meantime - perfect, if not, I do not want to block here on them.

solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
solidity/test/LPRewardsTest.js Outdated Show resolved Hide resolved
@pdyraga pdyraga merged commit 1601dce into master Dec 21, 2020
@pdyraga pdyraga deleted the liquidity-rewards branch December 21, 2020 13:47
nkuba added a commit to keep-network/keep-core that referenced this pull request Dec 21, 2020
BatchedPhasedEscrow


BatchedPhasedEsrow` is a token holder contract allowing contract owner to
approve a set of beneficiaries of tokens held by the contract, to
appoint a separate drawee role, and allowing that drawee to withdraw
tokens to approved beneficiaries in phases.

This contract is not the ultimate solution for a secure escrow with a
separate role withdrawing tokens. Notably, it does not limit the amount
or frequency of withdrawals so compromised drawee can drain the escrow
funds to one of the selected approved beneficiaries. The escrow owner
should proceed with caution and do not transfer all funds to
`BatchedPhasedEscrow`. Instead, funds should be transferred in tranches,
depending on how drawee spends them.

`CurveRewards` and `LPTokenRewards` contracts from `keep-ecdsa` that we are
are going to deploy for Uniswap pairs (see keep-network/keep-ecdsa#645) 
have the same `notifyRewardAmount` interface. To do not duplicate the code,
 I abstracted `CurveRewardsEscrowBeneficiary` contract into a generic
`StakingPoolRewardsEscrowBeneficiary` contract.
@pdyraga pdyraga added this to the solidity/v1.5.0 milestone Dec 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants