-
Notifications
You must be signed in to change notification settings - Fork 23
Conversation
Not interested in doing the OpenZeppelin and Solidity versioning dances. If we depart further from the contract than planned, we can integrate it properly into the rest of the repo
The hard-coded token address pattern is getting old.
Should be consturctor -> external -> public
There was a problem hiding this 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.
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. |
Indeed, this has been audited and in highly-visible production for the past 8 months. We just need confidence on the diff |
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:
|
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/test/LPRewardsTest.js
Outdated
}) | ||
|
||
describe("tokens allocation", () => { | ||
it("should successfully allocate KEEP tokens", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it("should successfully allocate KEEP tokens", async () => { | |
it("should successfully allocate KEEP tokens via receiveApproval function", async () => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
solidity/test/LPRewardsTest.js
Outdated
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
solidity/test/LPRewardsTest.js
Outdated
expect(keepBalance).to.eq.BN(rewards) | ||
}) | ||
|
||
it("should successfully allocate wrapped tokens", async () => { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) | ||
}) | ||
}) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
expect(keepBalance).to.eq.BN(rewards) | ||
}) | ||
|
||
it("should successfully allocate wrapped tokens", async () => { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
solidity/test/LPRewardsTest.js
Outdated
const future = (await time.latest()).add(time.duration.days(7)) | ||
await timeIncreaseTo(future) |
There was a problem hiding this comment.
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?
const future = (await time.latest()).add(time.duration.days(7)) | |
await timeIncreaseTo(future) | |
await timeIncreaseTo(await lpRewards.periodFinish.call()) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
expect(actualRewardPerToken).to.gte.BN( | ||
expectedRewardPerWrappedToken.subn(precision) | ||
) | ||
expect(actualRewardPerToken).to.lte.BN( | ||
expectedRewardPerWrappedToken.addn(precision) | ||
) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
expect(keepEarnedRewards).to.gte.BN(rewardsAmount.subn(precision)) | ||
expect(keepEarnedRewards).to.lte.BN(rewardsAmount.addn(precision)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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().
@dimpar Can you update this branch with |
There was a problem hiding this 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.
This reverts commit 4e8d014.
- removed a delay before increasing time on chain - fixing typo - removed initial balance check
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.
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 pairsTBTC/ETH
- https://info.uniswap.org/pair/0x854056fd40c1b52037166285b2e54fee774d33f6KEEP/ETH
- https://info.uniswap.org/pair/0xe6f19dab7d43317344282f803f8e8d240708174aKEEP/TBTC
- https://info.uniswap.org/pair/0x38c8ffee49f286f25d25bad919ff7552e5daf081