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

Feature/permit lock and process expired locks tests #42

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
- run:
name: slither-analysis
command: |
set +e
rm -rf foundry.toml
python -m slither . --filter-paths "node_modules|contracts/old|contracts/mock|contracts/interface" --solc-disable-warnings --json slither-analysis.json
python -m slither . --filter-paths "node_modules|contracts/old|contracts/mock|contracts/interface" --solc-disable-warnings --print human-summary,contract-summary
exit 0
Expand Down
49 changes: 44 additions & 5 deletions contracts/core/Relocker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ interface IRewardDistributor {
function claim(Common.Claim[] calldata claims) external;
}

interface IPermit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
}

contract Relocker {
using SafeTransferLib for ERC20;

Expand All @@ -29,6 +41,7 @@ contract Relocker {

error ZeroAddress();
error ZeroAmount();
error PermitFailed();

constructor(
address _btrfly,
Expand All @@ -49,18 +62,44 @@ contract Relocker {
/**
@notice Claim rewards based on the specified metadata and lock amount as rlBtrfly
@notice Use msg.sender not account parameter since relock is explicit action
@param claims Claim[] List of claim metadata
@param amount uint256 Amount to relock, cheaper to calculate offchain
@param claims Claim[] List of claim metadata
@param amount uint256 Amount to relock, cheaper to calculate offchain
@param _permitParams permit parameters for btrfly (optional)
*/
function claimAndLock(Common.Claim[] calldata claims, uint256 amount)
external
{
function claimAndLock(
Common.Claim[] calldata claims,
uint256 amount,
bytes calldata _permitParams
) external {
if (amount == 0) revert ZeroAmount();

// Claim rewards
rewardDistributor.claim(claims);

// Use Permit to transfer tokens to contract
_permit(_permitParams);

// Transfer amount to contract
btrfly.safeTransferFrom(msg.sender, address(this), amount);

// Lock amount as rlBtrfly
rlBtrfly.lock(msg.sender, amount);

emit Relock(msg.sender, amount);
}

/**
* @dev execute the permit according to the permit param
* @param _permitParams data
*/
function _permit(bytes calldata _permitParams) internal {
if (_permitParams.length == 32 * 7) {
(bool success, ) = address(btrfly).call(
abi.encodePacked(IPermit.permit.selector, _permitParams)
);
if (!success) {
revert PermitFailed();
}
}
}
}
7 changes: 1 addition & 6 deletions contracts/core/RewardDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,6 @@ contract RewardDistributor is Ownable, ReentrancyGuard {
require(sent, "Failed to transfer to account");
}

emit RewardClaimed(
token,
account,
claimable,
reward.updateCount
);
emit RewardClaimed(token, account, claimable, reward.updateCount);
}
}
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[default]
[profile.default]
src = "contracts"
test = "test/foundry"
out = "artifacts"
Expand Down
3 changes: 2 additions & 1 deletion lib/merkle/balance-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { BigNumber, utils } from 'ethers';

export default class BalanceTree {
private readonly tree: MerkleTree;
constructor(balances: { account: string; amount: BigNumber }[]) {
constructor(balances: { account: string; amount: BigNumber | string }[]) {
this.tree = new MerkleTree(
balances.map(({ account, amount }) => {
typeof amount === 'string' && (amount = BigNumber.from(amount));
return BalanceTree.toNode(account, amount);
})
);
Expand Down
192 changes: 163 additions & 29 deletions test/Relocker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import {
callAndReturnEvent,
claimData,
getPermitSignature,
impersonateAddressAndReturnSigner,
validateEvent,
} from './helpers';
Expand All @@ -15,6 +15,8 @@ import {
} from '../typechain';
import { expect } from 'chai';
import { BigNumber } from 'ethers';
import { BalanceTree } from '../lib/merkle';
import fetch from 'node-fetch';

type Claims = {
token: string;
Expand All @@ -23,22 +25,32 @@ type Claims = {
merkleProof: string[];
}[];

describe('Relocker', () => {
type Distribution = {
token: string;
merkleRoot: string;
proof: string;
};

describe('Relocker', function () {
let admin: SignerWithAddress;
let user: SignerWithAddress;
let actualUser: SignerWithAddress;
let mockUser: SignerWithAddress;
let rlBtrfly: RLBTRFLY;
let btrfly: BTRFLYV2;
let rewardDistributor: RewardDistributor;
let btrflyDistribution: Distribution;
let relocker: Relocker;
let claims: Claims;
let userBtrflyClaim: Claims;
let mockUserBtrflyClaim: Claims;
let btrflyAmount: string;
let snapshotId: number;

beforeEach(async function () {
({ admin } = this);
const arbitraryProof = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes('ARBITRARY_PROOF')
);

user = await impersonateAddressAndReturnSigner(
admin,
'0xe7ad7d90639a565fe3a6f68a41ad0b095f631f39'
);
before(async function () {
({ admin } = this);

rlBtrfly = (await ethers.getContractAt(
'RLBTRFLY',
Expand All @@ -55,6 +67,67 @@ describe('Relocker', () => {
'0xd7807E5752B368A6a64b76828Aaff0750522a76E'
)) as RewardDistributor;

//get latest distribution
const response = await fetch('https://raw.githubusercontent.com/redacted-cartel/distributions/master/protocol-v2/latest/btrfly.json');
const latestDistribution = await response.json();

//actualUser as the first account in latestDistribution with no claimed rewards
for (const item of latestDistribution) {
if ((await rewardDistributor.claimed(btrfly.address, item.account)).eq(0)) {
actualUser = await impersonateAddressAndReturnSigner(
admin,
item.account
);
btrflyAmount = item.amount;
break;
}
}

[mockUser] = await ethers.getSigners();

//append actualUser account to latest distribution
latestDistribution.push({
account: mockUser.address,
amount: btrflyAmount,
});

//get merkle tree from latest distribution
const btrflyTree = new BalanceTree(latestDistribution);

//impersonate reward distributor owner
const rewardDistributorOwner = await impersonateAddressAndReturnSigner(
admin,
await rewardDistributor.owner()
);

btrflyDistribution = {
token: btrfly.address,
merkleRoot: btrflyTree.getHexRoot(),
proof: arbitraryProof,
};

//update reward distributor with latest distribution
await rewardDistributor
.connect(rewardDistributorOwner)
.updateRewardsMetadata([btrflyDistribution]);

//get actualUser's merkle proof
userBtrflyClaim = [{
token: btrfly.address,
account: actualUser.address,
amount: btrflyAmount,
merkleProof: btrflyTree.getProof(actualUser.address, BigNumber.from(btrflyAmount)),
}];

mockUserBtrflyClaim = [{
token: btrfly.address,
account: mockUser.address,
amount: btrflyAmount,
merkleProof: btrflyTree.getProof(mockUser.address, BigNumber.from(btrflyAmount)),
}];
});

beforeEach(async function () {
const relockerFactory = (await ethers.getContractFactory(
'Relocker'
)) as Relocker__factory;
Expand All @@ -65,10 +138,10 @@ describe('Relocker', () => {
rewardDistributor.address
);

claims = claimData.map((data) => data.claimMetadata);
snapshotId = snapshotId ?? await ethers.provider.send('evm_snapshot', []);
});

describe('constructor', () => {
describe('constructor', function () {
it('Should set up contract state', async () => {
const btrflyAddress = await relocker.btrfly();
const rlBtrflyAddress = await relocker.rlBtrfly();
Expand All @@ -80,48 +153,109 @@ describe('Relocker', () => {
});
});

describe('claimAndLock', () => {
describe('claimAndLock', function () {
it('should revert on zero amount', async () => {
const amount = 0;

await expect(relocker.claimAndLock(claims, amount)).to.be.revertedWith(
await expect(relocker.claimAndLock(userBtrflyClaim, amount, "0x")).to.be.revertedWith(
'ZeroAmount()'
);
});

it('should claim and lock btrfly', async () => {
// isolate btrfly claim data
const btrflyClaims = claimData.filter(
(data) => data.token.toLowerCase() === btrfly.address.toLowerCase()
it('should revert on invalid permit signature', async () => {
//wrong address (AddressZero)
const { v, r, s } = await getPermitSignature(
mockUser,
btrfly,
ethers.constants.AddressZero,
BigNumber.from(btrflyAmount),
ethers.constants.Zero
);

const permitParams = ethers.utils.defaultAbiCoder.encode(
["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"],
[mockUser.address, ethers.constants.AddressZero, btrflyAmount, ethers.constants.Zero, v, r, s]
);

await expect(relocker.claimAndLock(mockUserBtrflyClaim, btrflyAmount, permitParams)).to.be.revertedWith("PermitFailed()");
});

it('should revert on expired permit deadline', async () => {
//get current block timestamp
const currentTimestamp = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp).add(-1);

//expired deadline
const { v, r, s } = await getPermitSignature(
mockUser,
btrfly,
relocker.address,
BigNumber.from(btrflyAmount),
currentTimestamp
);

// get total number of btrfly claimable by user
const btrflyAmount = btrflyClaims.reduce(
(prev, cur) => prev.add(BigNumber.from(cur.claimable)),
BigNumber.from(0)
const permitParams = ethers.utils.defaultAbiCoder.encode(
["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"],
[mockUser.address, relocker.address, btrflyAmount, currentTimestamp, v, r, s]
);

const userRLBalanceBefore = await rlBtrfly.lockedBalanceOf(user.address);
await expect(relocker.claimAndLock(mockUserBtrflyClaim, btrflyAmount, permitParams)).to.be.revertedWith("PermitFailed()");
});

it('should claim and lock btrfly using permit', async () => {
const userRLBalanceBefore = await rlBtrfly.lockedBalanceOf(mockUser.address);
const expectedUserRLBalanceAfter = userRLBalanceBefore.add(btrflyAmount);

await btrfly.connect(user).approve(relocker.address, btrflyAmount);
const { v, r, s } = await getPermitSignature(
mockUser,
btrfly,
relocker.address,
BigNumber.from(btrflyAmount),
ethers.constants.MaxUint256
)

const permitParams = ethers.utils.defaultAbiCoder.encode(
["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"],
[mockUser.address, relocker.address, btrflyAmount, ethers.constants.MaxUint256, v, r, s]
);

const relockEvent = await callAndReturnEvent(
relocker.connect(user).claimAndLock,
[claims, btrflyAmount]
relocker.connect(mockUser).claimAndLock,
[mockUserBtrflyClaim, btrflyAmount, permitParams]
);

validateEvent(relockEvent, 'Relock(address,uint256)', {
account: user.address,
account: mockUser.address,
amount: btrflyAmount,
});

const userRLBalanceAfter = await rlBtrfly.lockedBalanceOf(user.address);
const userRLBalanceAfter = await rlBtrfly.lockedBalanceOf(mockUser.address);
const userRLBalanceIncrease = userRLBalanceAfter.sub(userRLBalanceBefore);
expect(userRLBalanceAfter).to.equal(expectedUserRLBalanceAfter);
expect(userRLBalanceIncrease).to.equal(btrflyAmount);

await ethers.provider.send('evm_revert', [snapshotId]);
});

it('should claim and lock btrfly using approve', async () => {
const userRLBalanceBefore = await rlBtrfly.lockedBalanceOf(actualUser.address);
const expectedUserRLBalanceAfter = userRLBalanceBefore.add(btrflyAmount);
await btrfly.connect(actualUser).approve(relocker.address, btrflyAmount);

const relockEvent = await callAndReturnEvent(
relocker.connect(actualUser).claimAndLock,
[userBtrflyClaim, btrflyAmount, "0x"],
);

validateEvent(relockEvent, 'Relock(address,uint256)', {
account: actualUser.address,
amount: btrflyAmount,
});

const userRLBalanceAfter = await rlBtrfly.lockedBalanceOf(actualUser.address);
const userRLBalanceIncrease = userRLBalanceAfter.sub(userRLBalanceBefore);
expect(userRLBalanceAfter).to.equal(expectedUserRLBalanceAfter);
expect(userRLBalanceIncrease).to.equal(btrflyAmount);

await ethers.provider.send('evm_revert', [snapshotId]);
});
});
});
Loading