From 2d882d65da37670bbcc911087d031fa69cb20778 Mon Sep 17 00:00:00 2001 From: itofarina Date: Mon, 9 Oct 2023 15:18:52 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20escrow:=20receive=20`maxRatio`?= =?UTF-8?q?=20and=20`maxPeriod`=20on=20vesting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/strange-spoons-visit.md | 5 ++ .gas-snapshot | 69 ++++++++++----------- contracts/periphery/EscrowedEXA.sol | 19 +++++- test/EscrowedEXA.t.sol | 94 +++++++++++++++++++---------- 4 files changed, 119 insertions(+), 68 deletions(-) create mode 100644 .changeset/strange-spoons-visit.md diff --git a/.changeset/strange-spoons-visit.md b/.changeset/strange-spoons-visit.md new file mode 100644 index 000000000..fa52a1fb4 --- /dev/null +++ b/.changeset/strange-spoons-visit.md @@ -0,0 +1,5 @@ +--- +"@exactly/protocol": patch +--- + +🚸 escrow: receive `maxRatio` and `maxPeriod` on vesting diff --git a/.gas-snapshot b/.gas-snapshot index 8ce400c42..09fff76b3 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) diff --git a/contracts/periphery/EscrowedEXA.sol b/contracts/periphery/EscrowedEXA.sol index f6cf0c4c4..df0d0f7f2 100644 --- a/contracts/periphery/EscrowedEXA.sol +++ b/contracts/periphery/EscrowedEXA.sol @@ -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); @@ -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. @@ -225,6 +237,7 @@ contract EscrowedEXA is ERC20VotesUpgradeable, AccessControlUpgradeable { error Untransferable(); error InvalidStream(); +error Disagreement(); interface ISablierV2LockupLinear { function cancel(uint256 streamId) external; diff --git a/test/EscrowedEXA.t.sol b/test/EscrowedEXA.t.sol index 3d7bbee2f..a164dde8b 100644 --- a/test/EscrowedEXA.t.sol +++ b/test/EscrowedEXA.t.sol @@ -10,6 +10,7 @@ import { Broker, Durations, EscrowedEXA, + Disagreement, InvalidStream, Untransferable, CreateWithDurations, @@ -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"); @@ -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"); @@ -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 { @@ -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); @@ -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); @@ -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)); @@ -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); @@ -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); @@ -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)); @@ -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); @@ -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); @@ -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; @@ -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()); @@ -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()); @@ -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); @@ -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)); @@ -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;