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

[OZ:L-02] receive maxRatio and maxPeriod on vesting #673

Merged
merged 1 commit into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strange-spoons-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": patch
---

🚸 escrow: receive `maxRatio` and `maxPeriod` on vesting
69 changes: 35 additions & 34 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -93,40 +93,41 @@ DebtPreviewerTest:testPreviewLeverageSameWETHAssetMaxRatioMultipleCollateralAndD
DebtPreviewerTest:testPreviewLeverageSameWETHAssetMultipleCollateralAndDebtWithMinHealthFactor() (gas: 1708472)
DebtPreviewerTest:testPreviewMaxRatioWithdrawWithSameAssetLeverage() (gas: 1020516)
DebtPreviewerTest:testPreviewSameAssetInvalidLeverageShouldCapRatio() (gas: 910754)
EscrowedEXATest:testCancelExternalStreams() (gas: 452302)
EscrowedEXATest:testCancelExternalStreamsWithesEXACancel() (gas: 903873)
EscrowedEXATest:testCancelFromStreamAndGetReserveBack() (gas: 451714)
EscrowedEXATest:testCancelFromStreamJustCreated() (gas: 404796)
EscrowedEXATest:testCancelShouldDeleteReserves() (gas: 471143)
EscrowedEXATest:testCancelShouldGiveReservesBack() (gas: 676562)
EscrowedEXATest:testCancelTwiceShouldRevert() (gas: 456028)
EscrowedEXATest:testCancelWithInvalidAccount() (gas: 348283)
EscrowedEXATest:testFakeTokenWithesEXARecipient() (gas: 932349)
EscrowedEXATest:testGrantTransferrerRoleAsAdmin() (gas: 45812)
EscrowedEXATest:testMint() (gas: 165163)
EscrowedEXATest:testMintMoreThanBalance() (gas: 27564)
EscrowedEXATest:testMintToAnother() (gas: 167359)
EscrowedEXATest:testMintZero() (gas: 14185)
EscrowedEXATest:testRedeemAsNotRedeemer() (gas: 214783)
EscrowedEXATest:testRedeemAsRedeemer() (gas: 165497)
EscrowedEXATest:testRedeemAsRedeemerToAnother() (gas: 170399)
EscrowedEXATest:testSetReserveRatioAsAdmin() (gas: 24495)
EscrowedEXATest:testSetReserveRatioAsNotAdmin() (gas: 47318)
EscrowedEXATest:testSetReserveRatioAsZero() (gas: 16293)
EscrowedEXATest:testSetVestingPeriodAsAdmin() (gas: 24642)
EscrowedEXATest:testSetVestingPeriodAsNotAdmin() (gas: 47411)
EscrowedEXATest:testTransferToNotTransferrer() (gas: 174841)
EscrowedEXATest:testTransferToTransferrer() (gas: 210463)
EscrowedEXATest:testVest() (gas: 346917)
EscrowedEXATest:testVestToAnother() (gas: 403826)
EscrowedEXATest:testVestToAnotherAndCancel() (gas: 489580)
EscrowedEXATest:testVestWithPermitReserve() (gas: 458876)
EscrowedEXATest:testVestZero() (gas: 14151)
EscrowedEXATest:testWithdrawFromStreamAndGetReserveBack() (gas: 329850)
EscrowedEXATest:testWithdrawFromUnknownStream() (gas: 898481)
EscrowedEXATest:testWithdrawMaxFromMultipleStreams() (gas: 1007617)
EscrowedEXATest:testWithdrawMaxShouldGiveReserveBackWhenDepleted() (gas: 327862)
EscrowedEXATest:testWithdrawMaxWithInvalidSender() (gas: 339700)
EscrowedEXATest:testCancelExternalStreams() (gas: 452412)
EscrowedEXATest:testCancelExternalStreamsWithesEXACancel() (gas: 903980)
EscrowedEXATest:testCancelFromStreamAndGetReserveBack() (gas: 453373)
EscrowedEXATest:testCancelFromStreamJustCreated() (gas: 406436)
EscrowedEXATest:testCancelShouldDeleteReserves() (gas: 472647)
EscrowedEXATest:testCancelShouldGiveReservesBack() (gas: 679619)
EscrowedEXATest:testCancelTwiceShouldRevert() (gas: 458917)
EscrowedEXATest:testCancelWithInvalidAccount() (gas: 349851)
EscrowedEXATest:testFakeTokenWithesEXARecipient() (gas: 932415)
EscrowedEXATest:testGrantTransferrerRoleAsAdmin() (gas: 45878)
EscrowedEXATest:testMint() (gas: 165075)
EscrowedEXATest:testMintMoreThanBalance() (gas: 27454)
EscrowedEXATest:testMintToAnother() (gas: 167271)
EscrowedEXATest:testMintZero() (gas: 14075)
EscrowedEXATest:testRedeemAsNotRedeemer() (gas: 214695)
EscrowedEXATest:testRedeemAsRedeemer() (gas: 165427)
EscrowedEXATest:testRedeemAsRedeemerToAnother() (gas: 170328)
EscrowedEXATest:testSetReserveRatioAsAdmin() (gas: 24539)
EscrowedEXATest:testSetReserveRatioAsNotAdmin() (gas: 47340)
EscrowedEXATest:testSetReserveRatioAsZero() (gas: 16315)
EscrowedEXATest:testSetVestingPeriodAsAdmin() (gas: 24686)
EscrowedEXATest:testSetVestingPeriodAsNotAdmin() (gas: 47433)
EscrowedEXATest:testTransferToNotTransferrer() (gas: 174816)
EscrowedEXATest:testTransferToTransferrer() (gas: 210419)
EscrowedEXATest:testVest() (gas: 348466)
EscrowedEXATest:testVestDisagreement() (gas: 181934)
EscrowedEXATest:testVestToAnother() (gas: 405315)
EscrowedEXATest:testVestToAnotherAndCancel() (gas: 491167)
EscrowedEXATest:testVestWithPermitReserve() (gas: 459061)
EscrowedEXATest:testVestZero() (gas: 20986)
EscrowedEXATest:testWithdrawFromStreamAndGetReserveBack() (gas: 331358)
EscrowedEXATest:testWithdrawFromUnknownStream() (gas: 898393)
EscrowedEXATest:testWithdrawMaxFromMultipleStreams() (gas: 1015334)
EscrowedEXATest:testWithdrawMaxShouldGiveReserveBackWhenDepleted() (gas: 329348)
EscrowedEXATest:testWithdrawMaxWithInvalidSender() (gas: 342466)
InterestRateModelTest:testFixedBorrowRate() (gas: 8089)
InterestRateModelTest:testFloatingBorrowRate() (gas: 6236)
InterestRateModelTest:testMinFixedRate() (gas: 7610)
Expand Down
19 changes: 16 additions & 3 deletions contracts/periphery/EscrowedEXA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,13 @@ contract EscrowedEXA is ERC20VotesUpgradeable, AccessControlUpgradeable {
/// @notice Starts a vesting stream.
/// @param amount Amount of EXA to vest.
/// @param to Address to vest to.
/// @param maxRatio Maximum reserve ratio accepted for the vesting.
/// @param maxPeriod Maximum vesting period accepted for the vesting.
/// @return streamId of the vesting stream.
function vest(uint128 amount, address to) public returns (uint256 streamId) {
function vest(uint128 amount, address to, uint256 maxRatio, uint256 maxPeriod) public returns (uint256 streamId) {
assert(amount != 0);
if (reserveRatio > maxRatio || vestingPeriod > maxPeriod) revert Disagreement();

_burn(msg.sender, amount);
uint256 reserve = amount.mulWadUp(reserveRatio);
exa.safeTransferFrom(msg.sender, address(this), reserve);
Expand All @@ -107,11 +111,19 @@ contract EscrowedEXA is ERC20VotesUpgradeable, AccessControlUpgradeable {
/// @notice Starts a vesting stream using a permit.
/// @param amount Amount of EXA to vest.
/// @param to Address to vest to.
/// @param maxRatio Maximum reserve ratio accepted for the vesting.
/// @param maxPeriod Maximum vesting period accepted for the vesting.
/// @param p Permit for the EXA reserve.
/// @return streamId of the vesting stream.
function vest(uint128 amount, address to, Permit calldata p) external returns (uint256 streamId) {
function vest(
uint128 amount,
address to,
uint256 maxRatio,
uint256 maxPeriod,
Permit calldata p
) external returns (uint256 streamId) {
exa.safePermit(msg.sender, address(this), p.value, p.deadline, p.v, p.r, p.s);
return vest(amount, to);
return vest(amount, to, maxRatio, maxPeriod);
}

/// @notice Cancels vesting streams.
Expand Down Expand Up @@ -225,6 +237,7 @@ contract EscrowedEXA is ERC20VotesUpgradeable, AccessControlUpgradeable {

error Untransferable();
error InvalidStream();
error Disagreement();

interface ISablierV2LockupLinear {
function cancel(uint256 streamId) external;
Expand Down
94 changes: 63 additions & 31 deletions test/EscrowedEXA.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Broker,
Durations,
EscrowedEXA,
Disagreement,
InvalidStream,
Untransferable,
CreateWithDurations,
Expand Down Expand Up @@ -72,14 +73,15 @@ contract EscrowedEXATest is ForkTest {

function testVest() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);

esEXA.mint(amount, address(this));
uint256 exaBefore = exa.balanceOf(address(this));
uint256 nextStreamId = ISablierV2Lockup(address(sablier)).nextStreamId();
vm.expectEmit(true, true, true, true, address(esEXA));
emit Vest(address(this), address(this), nextStreamId, amount);
uint256 streamId = esEXA.vest(uint128(amount), address(this));
uint256 streamId = esEXA.vest(uint128(amount), address(this), ratio, esEXA.vestingPeriod());

assertEq(exaBefore, exa.balanceOf(address(this)) + reserve, "exa balance of sender -= reserve");
assertEq(exa.balanceOf(address(esEXA)), reserve, "exa balance of esEXA == reserve");
Expand All @@ -89,14 +91,15 @@ contract EscrowedEXATest is ForkTest {

function testVestToAnother() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);

esEXA.mint(amount, address(this));
uint256 exaBefore = exa.balanceOf(address(this));
uint256 nextStreamId = ISablierV2Lockup(address(sablier)).nextStreamId();
vm.expectEmit(true, true, true, true, address(esEXA));
emit Vest(address(this), alice, nextStreamId, amount);
uint256 streamId = esEXA.vest(uint128(amount), alice);
uint256 streamId = esEXA.vest(uint128(amount), alice, ratio, esEXA.vestingPeriod());

assertEq(exaBefore, exa.balanceOf(address(this)) + reserve, "exa balance of sender -= reserve");
assertEq(exa.balanceOf(address(esEXA)), reserve, "exa balance of esEXA == reserve");
Expand All @@ -114,8 +117,10 @@ contract EscrowedEXATest is ForkTest {
}

function testVestZero() external {
uint256 ratio = esEXA.reserveRatio();
uint256 period = esEXA.vestingPeriod();
vm.expectRevert(stdError.assertionError);
esEXA.vest(0, address(this));
esEXA.vest(0, address(this), ratio, period);
}

function testSetVestingPeriodAsAdmin() external {
Expand Down Expand Up @@ -167,15 +172,16 @@ contract EscrowedEXATest is ForkTest {

function testCancelShouldGiveReservesBack() external {
uint256 initialAmount = 1_000 ether;
uint256 reserve = initialAmount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = initialAmount.mulWadDown(ratio);
esEXA.mint(initialAmount * 2, address(this));

uint256 initialEXA = exa.balanceOf(address(this));

uint256 streamId1 = esEXA.vest(uint128(initialAmount), address(this));
uint256 streamId1 = esEXA.vest(uint128(initialAmount), address(this), ratio, esEXA.vestingPeriod());
assertEq(exa.balanceOf(address(this)), initialEXA - reserve);

uint256 streamId2 = esEXA.vest(uint128(initialAmount), address(this));
uint256 streamId2 = esEXA.vest(uint128(initialAmount), address(this), ratio, esEXA.vestingPeriod());
assertEq(exa.balanceOf(address(this)), initialEXA - reserve * 2);

vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);
Expand All @@ -196,11 +202,12 @@ contract EscrowedEXATest is ForkTest {

function testVestToAnotherAndCancel() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);

esEXA.mint(amount, address(this));
uint256[] memory streams = new uint256[](1);
streams[0] = esEXA.vest(uint128(amount), alice);
streams[0] = esEXA.vest(uint128(amount), alice, ratio, esEXA.vestingPeriod());

vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);
vm.prank(alice);
Expand All @@ -212,10 +219,11 @@ contract EscrowedEXATest is ForkTest {

function testCancelShouldDeleteReserves() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);
esEXA.mint(amount, address(this));

uint256 streamId = esEXA.vest(uint128(amount), address(this));
uint256 streamId = esEXA.vest(uint128(amount), address(this), ratio, esEXA.vestingPeriod());
vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);

uint256 exaBefore = exa.balanceOf(address(this));
Expand All @@ -234,7 +242,7 @@ contract EscrowedEXATest is ForkTest {
uint256 amount = 1_000 ether;
esEXA.mint(amount, address(this));

uint256 streamId = esEXA.vest(uint128(amount), address(this));
uint256 streamId = esEXA.vest(uint128(amount), address(this), esEXA.reserveRatio(), esEXA.vestingPeriod());
vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);

uint256[] memory streamIds = new uint256[](1);
Expand All @@ -246,12 +254,13 @@ contract EscrowedEXATest is ForkTest {

function testCancelWithInvalidAccount() external {
uint256 initialAmount = 1_000 ether;
uint256 reserve = initialAmount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = initialAmount.mulWadDown(ratio);
esEXA.mint(initialAmount, address(this));

uint256 initialEXA = exa.balanceOf(address(this));

uint256 streamId1 = esEXA.vest(uint128(initialAmount), address(this));
uint256 streamId1 = esEXA.vest(uint128(initialAmount), address(this), ratio, esEXA.vestingPeriod());
assertEq(exa.balanceOf(address(this)), initialEXA - reserve);

vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);
Expand All @@ -266,17 +275,18 @@ contract EscrowedEXATest is ForkTest {
function testWithdrawMaxFromMultipleStreams() external {
uint256 initialAmount = 1_000 ether;
esEXA.mint(initialAmount, address(this));
uint256 ratio = esEXA.reserveRatio();
uint256[] memory streams = new uint256[](4);
streams[0] = esEXA.vest(uint128(200 ether), address(this));
streams[0] = esEXA.vest(uint128(200 ether), address(this), ratio, esEXA.vestingPeriod());

vm.warp(block.timestamp + 2 days);
streams[1] = esEXA.vest(uint128(300 ether), address(this));
streams[1] = esEXA.vest(uint128(300 ether), address(this), ratio, esEXA.vestingPeriod());

vm.warp(block.timestamp + 7 days);
streams[2] = esEXA.vest(uint128(100 ether), address(this));
streams[2] = esEXA.vest(uint128(100 ether), address(this), ratio, esEXA.vestingPeriod());

vm.warp(block.timestamp + 3 weeks);
streams[3] = esEXA.vest(uint128(400 ether), address(this));
streams[3] = esEXA.vest(uint128(400 ether), address(this), ratio, esEXA.vestingPeriod());

vm.warp(block.timestamp + 5 weeks);
uint256 balanceEXA = exa.balanceOf(address(this));
Expand All @@ -295,7 +305,7 @@ contract EscrowedEXATest is ForkTest {
uint256 initialAmount = 1_000 ether;
esEXA.mint(initialAmount, address(this));
uint256[] memory streams = new uint256[](1);
streams[0] = esEXA.vest(uint128(1_000 ether), address(this));
streams[0] = esEXA.vest(uint128(1_000 ether), address(this), esEXA.reserveRatio(), esEXA.vestingPeriod());

vm.warp(block.timestamp + 5 weeks);
vm.prank(alice);
Expand Down Expand Up @@ -337,7 +347,8 @@ contract EscrowedEXATest is ForkTest {
function testVestWithPermitReserve() external {
uint256 amount = 1_000 ether;
esEXA.mint(amount, alice);
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);
exa.transfer(alice, reserve);

uint256 exaBefore = exa.balanceOf(alice);
Expand All @@ -362,18 +373,19 @@ contract EscrowedEXATest is ForkTest {
)
)
);
uint256 period = esEXA.vestingPeriod();

vm.expectEmit(true, true, true, true, address(esEXA));
emit Vest(alice, alice, nextStreamId, amount);
vm.prank(alice);
uint256 streamId = esEXA.vest(uint128(amount), alice, Permit(reserve, block.timestamp, v, r, s));
uint256 streamId = esEXA.vest(uint128(amount), alice, ratio, period, Permit(reserve, block.timestamp, v, r, s));

assertEq(exaBefore, exa.balanceOf(alice) + reserve, "exa balance of alice -= reserve");
assertEq(exa.balanceOf(address(esEXA)), reserve, "exa balance of esEXA == reserve");
assertGt(streamId, 0);
assertEq(streamId, nextStreamId);

vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);
vm.warp(block.timestamp + period / 2);

uint256[] memory streams = new uint256[](1);
streams[0] = streamId;
Expand All @@ -384,10 +396,11 @@ contract EscrowedEXATest is ForkTest {

function testWithdrawMaxShouldGiveReserveBackWhenDepleted() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);
esEXA.mint(amount, address(this));
uint256[] memory streams = new uint256[](1);
streams[0] = esEXA.vest(uint128(amount), address(this));
streams[0] = esEXA.vest(uint128(amount), address(this), ratio, esEXA.vestingPeriod());

uint256 exaBefore = exa.balanceOf(address(this));
vm.warp(block.timestamp + esEXA.vestingPeriod());
Expand All @@ -397,10 +410,11 @@ contract EscrowedEXATest is ForkTest {

function testWithdrawFromStreamAndGetReserveBack() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);
esEXA.mint(amount, address(this));
uint256[] memory streams = new uint256[](1);
streams[0] = esEXA.vest(uint128(amount), address(this));
streams[0] = esEXA.vest(uint128(amount), address(this), ratio, esEXA.vestingPeriod());

uint256 exaBefore = exa.balanceOf(address(this));
vm.warp(block.timestamp + esEXA.vestingPeriod());
Expand All @@ -414,9 +428,10 @@ contract EscrowedEXATest is ForkTest {

function testCancelFromStreamAndGetReserveBack() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);
esEXA.mint(amount, address(this));
uint256 streamId = esEXA.vest(uint128(amount), address(this));
uint256 streamId = esEXA.vest(uint128(amount), address(this), ratio, esEXA.vestingPeriod());

uint256 exaBefore = exa.balanceOf(address(this));
vm.warp(block.timestamp + esEXA.vestingPeriod() / 2);
Expand All @@ -431,10 +446,11 @@ contract EscrowedEXATest is ForkTest {

function testCancelFromStreamJustCreated() external {
uint256 amount = 1_000 ether;
uint256 reserve = amount.mulWadDown(esEXA.reserveRatio());
uint256 ratio = esEXA.reserveRatio();
uint256 reserve = amount.mulWadDown(ratio);
esEXA.mint(amount, address(this));
uint256[] memory streams = new uint256[](1);
streams[0] = esEXA.vest(uint128(amount), address(this));
streams[0] = esEXA.vest(uint128(amount), address(this), ratio, esEXA.vestingPeriod());

uint256 exaBefore = exa.balanceOf(address(this));

Expand Down Expand Up @@ -532,6 +548,22 @@ contract EscrowedEXATest is ForkTest {
esEXA.setReserveRatio(0);
}

function testVestDisagreement() external {
uint256 amount = 1_000 ether;
uint256 ratio = esEXA.reserveRatio();
uint256 period = esEXA.vestingPeriod();
esEXA.mint(amount, address(this));

vm.expectRevert(Disagreement.selector);
esEXA.vest(uint128(amount), address(this), ratio, period - 1);

vm.expectRevert(Disagreement.selector);
esEXA.vest(uint128(amount), address(this), ratio - 1, period);

vm.expectRevert(Disagreement.selector);
esEXA.vest(uint128(amount), address(this), ratio - 1, period - 1);
}

function testWithdrawFromUnknownStream() external {
MockERC20 token = new MockERC20("Random token", "RNT", 18);
uint128 amount = 1_000 ether;
Expand Down