Skip to content
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

Merged
merged 18 commits into from Nov 3, 2022
Merged

Conversation

nicholaspai
Copy link
Member

@nicholaspai nicholaspai commented Oct 27, 2022

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 the ClaimAndStake 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.

To make dApp more useful, add method to claim from external merkle distributor and stake atomically
@nicholaspai nicholaspai marked this pull request as ready for review October 28, 2022 20:47
Copy link

@dohaki dohaki left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@nicholaspai nicholaspai marked this pull request as draft October 31, 2022 21:18
Copy link
Contributor

@pxrl pxrl left a 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.

@nicholaspai nicholaspai marked this pull request as ready for review November 1, 2022 20:49

UserDeposit storage userDeposit = stakingTokens[stakedToken].stakingBalances[staker];

uint256 averageDepositTime = getAverageDepositTimePostDeposit(stakedToken, staker, amount);
Copy link
Contributor

@kevinuma kevinuma Nov 2, 2022

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;

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Contributor

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?

Copy link
Contributor

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.

contracts/AcceleratingDistributorClaimAndStake.sol Outdated Show resolved Hide resolved
test/AcceleratingDistributor.ClaimAndStake.ts Outdated Show resolved Hide resolved

UserDeposit storage userDeposit = stakingTokens[stakedToken].stakingBalances[staker];

uint256 averageDepositTime = getAverageDepositTimePostDeposit(stakedToken, staker, amount);
Copy link
Contributor

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)
Copy link
Contributor

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 process has a few advantages:

  1. Lighter modifications to the AcceleratingDistributor (still some, though), which holds user funds.
  2. Single transaction claim (no approvals).

Downsides:

  1. More cross contract calls.
  2. One more contract to deploy.

Copy link
Member Author

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

@nicholaspai nicholaspai changed the title feat: Add claimAndStake method feat: Add ClaimAndStake contract Nov 2, 2022
@@ -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);
Copy link
Member Author

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

Copy link
Contributor

@mrice32 mrice32 left a 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

event SetMerkleDistributor(AcrossMerkleDistributorInterface indexed newMerkleDistributor);
event SetAcceleratingDistributor(AcceleratingDistributorInterface indexed newAcceleratingDistributor);

constructor() {}
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Member Author

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

*/
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");
Copy link
Contributor

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?

Copy link
Member Author

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.

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);
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

@mrice32 mrice32 left a 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

* AcceleratingDistributor to spend its staking tokens.
*/

contract ClaimAndStake is ReentrancyGuard, Ownable, Multicall {
Copy link
Contributor

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?

Copy link
Member Author

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

Comment on lines 11 to 21
interface AcceleratingDistributorInterface {
function stakeFor(
address beneficiary,
address stakedToken,
uint256 amount
) external;
}

interface AcrossMerkleDistributorInterface is MerkleDistributorInterface {
function claimFor(MerkleDistributorInterface.Claim memory _claim) external;
}
Copy link
Contributor

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).

Copy link
Member Author

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

Comment on lines 197 to 202
function stakeFor(
address beneficiary,
address stakedToken,
uint256 amount
) external nonReentrant onlyEnabled(stakedToken) {
_stake(stakedToken, amount, beneficiary);
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Member Author

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);
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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 {
Copy link
Contributor

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

Copy link
Member Author

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()

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright sounds good

Copy link
Contributor

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

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

LGTM!

@nicholaspai nicholaspai merged commit dde7aed into master Nov 3, 2022
@mrice32 mrice32 deleted the npai/claim-and-stake branch November 3, 2022 16:57
* @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 {
Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants