Skip to content

Commit

Permalink
馃毟 escrow: receive maxRatio and maxPeriod on vesting
Browse files Browse the repository at this point in the history
  • Loading branch information
itofarina committed Oct 11, 2023
1 parent 0182437 commit 2d882d6
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 68 deletions.
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

0 comments on commit 2d882d6

Please sign in to comment.