New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add ClaimAndStake contract #32
Conversation
To make dApp more useful, add method to claim from external merkle distributor and stake atomically
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.
LGTM 👍
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 given this one pass over and don't have any comments. I'll take another look tomorrow, with fresh eyes.
|
||
UserDeposit storage userDeposit = stakingTokens[stakedToken].stakingBalances[staker]; | ||
|
||
uint256 averageDepositTime = getAverageDepositTimePostDeposit(stakedToken, staker, amount); |
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.
This is existing code but in getAverageDepositTimePostDeposit, I see
uint256 amountWeightedTime = (((amount * 1e18) / (userDeposit.cumulativeBalance + amount)) *
(getTimeSinceAverageDeposit(stakedToken, account))) / 1e18;
Why not multiple first before dividing by (userDeposit.cumulativeBalance + amount) to minimize rounding errors? Also there are way too many parentheses there.
uint256 amountWeightedTime = (amount * 1e18) * getTimeSinceAverageDeposit(stakedToken, account) / (userDeposit.cumulativeBalance + amount) / 1e18;
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.
These are good points but I don't love modifying existing already-audited code. @chrismaree wdyt?
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 dont think we touch this.
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.
@kevinuma In general, we would prefer to modify the code as little as possible. The only exception is if we would classify this as a bug. Is the rounding error substantial enough to consider it a bug?
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.
The rounding error is likely not significant unless a user has staked a significant amount of tokens and then add a very small amount.
|
||
UserDeposit storage userDeposit = stakingTokens[stakedToken].stakingBalances[staker]; | ||
|
||
uint256 averageDepositTime = getAverageDepositTimePostDeposit(stakedToken, staker, amount); |
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.
@kevinuma In general, we would prefer to modify the code as little as possible. The only exception is if we would classify this as a bug. Is the rounding error substantial enough to consider it a bug?
* @param claims Claim leaves to retrieve from MerkleDistributor. | ||
* @param stakedToken The address of the token to stake. | ||
*/ | ||
function claimAndStake(MerkleDistributorInterface.Claim[] memory claims, address stakedToken) |
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 don't want to throw a wrench into anything, but I was brainstorming a bit and wanted to ask about a slightly different implementation/strategy.
This would introduce less code in this contract, but may add more code (or more complex code) in the merkle distributor and may have a slightly better business outcome (one transaction, no approval):
- Add a stakeFor() method in this contract (basically just a wrapper function around _stake() that you have above).
- Drop the claimAndStake() method.
- Add a claimFor method in the merkle distributor that allows a contract to claim on behalf of a user (and actually receive the tokens). This could be done via signature or whitelist.
- Build a special intermediary contract that does the following:
- This contract would have a single method to do 2 things:
- Claim their airdrop (and pull it into this contract).
- Call stakeFor() to stake on behalf of the user.
- This contract would have a single method to do 2 things:
This process has a few advantages:
- Lighter modifications to the AcceleratingDistributor (still some, though), which holds user funds.
- Single transaction claim (no approvals).
Downsides:
- More cross contract calls.
- One more contract to deploy.
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 this is a good idea I'll start work on the AcrossMerkleDistributor
@@ -182,26 +182,24 @@ contract AcceleratingDistributor is ReentrancyGuard, Ownable, Multicall { | |||
* @param amount The amount of the token to stake. | |||
*/ | |||
function stake(address stakedToken, uint256 amount) external nonReentrant onlyEnabled(stakedToken) { | |||
_updateReward(stakedToken, msg.sender); |
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.
This should be copied exactly to new _stake
internal method
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.
Looks awesome! Only minor comments
contracts/ClaimAndStake.sol
Outdated
event SetMerkleDistributor(AcrossMerkleDistributorInterface indexed newMerkleDistributor); | ||
event SetAcceleratingDistributor(AcceleratingDistributorInterface indexed newAcceleratingDistributor); | ||
|
||
constructor() {} |
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.
Did you want to set the merkleDistributor and acceleratingDistributor contracts as immutable? IMO, this would reduce security surface area and have few (if any) downsides since this contract could always be redeployed with different parameters.
Thoughts?
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.
To be honest the main reason why I didn't do this is because I couldn't figure out how to get the deploy
scripts to work with a MerkleDistributor contract address imported from contracts-v2
. I'll re review
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.
Ok this was easy since we import contracts-v2
contracts/ClaimAndStake.sol
Outdated
*/ | ||
function claimAndStake(MerkleDistributorInterface.Claim memory _claim, address stakedToken) external nonReentrant { | ||
require(_claim.account == msg.sender, "claim account not caller"); | ||
require(merkleDistributor.getRewardTokenForWindow(_claim.windowIndex) == stakedToken, "unexpected claim token"); |
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.
nit: why not drop the stakedToken argument and pull it from the merkleDistributor directly?
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.
Good point, the contract should revert if the claimed token isn't enabled for staking.
contracts/ClaimAndStake.sol
Outdated
require(_claim.account == msg.sender, "claim account not caller"); | ||
require(merkleDistributor.getRewardTokenForWindow(_claim.windowIndex) == stakedToken, "unexpected claim token"); | ||
merkleDistributor.claimFor(_claim); | ||
IERC20(stakedToken).safeApprove(address(acceleratingDistributor), _claim.amount); |
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 wanted to get your thoughts on the use of this function. We usually prefer to not use it, but it might actually be good to use it in this case.
See this check.
I was originally thinking that it would be bad if a leftover approval from a previous call caused the entire contract to begin reverting. However, maybe the opposite is true. A leftover nonzero approval might mean that there is a bug in the contract, so it could be preferable for it to begin reverting in that case.
This probably doesn't matter much, but the alternative would be this function if we wanted to not fail if there was leftover approval. Thoughts?
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.
Oh good point this was an oversight i think its always safer to use safeIncreaseAllowance
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.
LGTM! Only minor nits
contracts/ClaimAndStake.sol
Outdated
* AcceleratingDistributor to spend its staking tokens. | ||
*/ | ||
|
||
contract ClaimAndStake is ReentrancyGuard, Ownable, Multicall { |
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.
nit: why does this contract need to be ownable?
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 no longer does! now that I've removed the setDistributorContracts
contracts/ClaimAndStake.sol
Outdated
interface AcceleratingDistributorInterface { | ||
function stakeFor( | ||
address beneficiary, | ||
address stakedToken, | ||
uint256 amount | ||
) external; | ||
} | ||
|
||
interface AcrossMerkleDistributorInterface is MerkleDistributorInterface { | ||
function claimFor(MerkleDistributorInterface.Claim memory _claim) external; | ||
} |
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.
nit: I know we do this elsewhere, but is there any reason not to just import these contracts directly? I only suggest it since it minimizes the chance of error (like a typo in the minimal interface that doesn't match the real 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.
For contract size purposes but I guess that's not really a concern since this contract is so small
function stakeFor( | ||
address beneficiary, | ||
address stakedToken, | ||
uint256 amount | ||
) external nonReentrant onlyEnabled(stakedToken) { | ||
_stake(stakedToken, amount, beneficiary); |
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.
tiny nit: any reason the args are in a different order in stakeFor vs _stake?
Note: no need to change if there is some reason, just a random thing I noticed.
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.
ah good catch ill add benefciary
last
/** | ||
* @notice Stake tokens for rewards on behalf of `beneficiary`. | ||
* @dev The caller of this function must approve this contract to spend amount of stakedToken. | ||
* @dev The caller of this function is effectively donating their tokens to the beneficiary. The beneficiary |
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.
+1
function claimAndStake(MerkleDistributorInterface.Claim memory _claim) external nonReentrant { | ||
require(_claim.account == msg.sender, "claim account not caller"); | ||
address stakedToken = merkleDistributor.getRewardTokenForWindow(_claim.windowIndex); | ||
merkleDistributor.claimFor(_claim); |
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.
Should we assert that this contract has received the right claim amount for additional safety?
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 don't think we need to since we can assume that claimFor
works as expected and because the merkleDistributor
address is a trusted one (and set upon deployment). If we assume claimFor
works
function claimAndStake(MerkleDistributorInterface.Claim memory _claim) external nonReentrant { | ||
require(_claim.account == msg.sender, "claim account not caller"); | ||
address stakedToken = merkleDistributor.getRewardTokenForWindow(_claim.windowIndex); | ||
merkleDistributor.claimFor(_claim); |
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.
To call claimFor, this contract has to be whitelisted in the merkle distributor contract: https://github.com/across-protocol/contracts-v2/blob/master/contracts/merkle-distributor/AcrossMerkleDistributor.sol#L41. I don't see this in the deployment step. Is this something we'll do separately?
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.
yep! Usually we use the deployment scripts jsut to deploy contracts, and all the setup is done afterwards either manually or via scripts
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 was thinking it could be a step in the same deployment script as ClaimAndStake if the deployer still has ownership if merkle distributor. It's less manual and ensures that we don't need to make sure the addresses are right later.
* @dev Will revert if this contract is not a "whitelisted claimer" on the MerkleDistributor contract. | ||
* @param _claim Claim leaf to retrieve from MerkleDistributor. | ||
*/ | ||
function claimAndStake(MerkleDistributorInterface.Claim memory _claim) external nonReentrant { |
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 wonder if we can add a multi claim function as well since merkle distributor supports claimMulti anyway
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.
This contract is Multicaller
so we can just multicall it. I think this is preferable to implementing claimMultiFor
which would require re-writing most of the logic in claimMulti
to be rewritten. This is because the claimMulti
code doesn't simply call claim()
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.
Alright sounds good
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.
LGTM!
* @dev Will revert if this contract is not a "whitelisted claimer" on the MerkleDistributor contract. | ||
* @param _claim Claim leaf to retrieve from MerkleDistributor. | ||
*/ | ||
function claimAndStake(MerkleDistributorInterface.Claim memory _claim) external nonReentrant { |
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.
New claimAndStake function that atomically calls claimFor
on AcrossMerkleDistributor and then calls stakeFor
for original claimer.
To make dApp more useful, add new contract
ClaimAndStake
that can atomically claim on behalf of a user from a MerkleDistributor and stake into the AcceleratingDistributor. The user does not need to sign an allowance, they only need to trust theClaimAndStake
contract to correctly stake on their behalf.This PR adds a new
stakeFor
method to the AcceleratingDistributor that enables this claim and stake to work atomically.