diff --git a/contracts/BondWrapper.sol b/contracts/BondWrapper.sol index 4db90cc35..e22c0239a 100644 --- a/contracts/BondWrapper.sol +++ b/contracts/BondWrapper.sol @@ -68,10 +68,12 @@ contract BondWrapper is ERC20Permit { /// @param maturityTime The bond's expiry time /// @param amount The amount of bonds to redeem /// @param andBurn If true it will burn the number of erc20 minted by this deposited bond + /// @param destination The address which gets credited with this withdraw function close( uint256 maturityTime, uint256 amount, - bool andBurn + bool andBurn, + address destination ) external { // Encode the asset ID uint256 assetId = AssetId.encodeAssetId( @@ -89,7 +91,12 @@ contract BondWrapper is ERC20Permit { uint256 receivedAmount; if (forceClosed == 0) { // Close the bond [selling if earlier than the expiration] - receivedAmount = hyperdrive.closeLong(maturityTime, amount, 0); + receivedAmount = hyperdrive.closeLong( + maturityTime, + amount, + 0, + address(this) + ); // Update the user account data, note this sub is safe because the top bits are zero. userAccounts[msg.sender][assetId] -= amount; } else { @@ -117,7 +124,7 @@ contract BondWrapper is ERC20Permit { } // Transfer the released funds to the user - bool success = token.transfer(msg.sender, userFunds); + bool success = token.transfer(destination, userFunds); if (!success) revert Errors.TransferFailed(); } @@ -150,7 +157,8 @@ contract BondWrapper is ERC20Permit { uint256 receivedAmount = hyperdrive.closeLong( maturityTime, deposited, - 0 + 0, + address(this) ); // Store the user account update userAccounts[user][assetId] = (receivedAmount << 128) + deposited; diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 122087acd..1a68f1219 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -187,10 +187,12 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @notice Allows LPs to supply liquidity for LP shares. /// @param _contribution The amount of base to supply. /// @param _minOutput The minimum number of LP tokens the user should receive + /// @param _destination The address which will hold the LP shares /// @return lpShares The number of LP tokens created function addLiquidity( uint256 _contribution, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256 lpShares) { if (_contribution == 0) { revert Errors.ZeroAmount(); @@ -238,7 +240,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { ); // Mint LP shares to the supplier. - _mint(AssetId._LP_ASSET_ID, msg.sender, lpShares); + _mint(AssetId._LP_ASSET_ID, _destination, lpShares); } // TODO: Consider if some MEV protection is necessary for the LP. @@ -249,11 +251,13 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// value is likely to be less than the amount LP shares are worth. /// The remainder is in short and long withdraw shares which are hard /// to game the value of. + /// @param _destination The address which will receive the withdraw proceeds /// @return Returns the base out, the lond withdraw shares out and the short withdraw /// shares out. function removeLiquidity( uint256 _shares, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256, uint256, uint256) { if (_shares == 0) { revert Errors.ZeroAmount(); @@ -309,7 +313,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Mint the long and short withdrawal tokens. _mint( AssetId.encodeAssetId(AssetId.AssetIdPrefix.LongWithdrawalShare, 0), - msg.sender, + _destination, longWithdrawalShares ); longWithdrawalSharesOutstanding += longWithdrawalShares; @@ -318,14 +322,14 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { AssetId.AssetIdPrefix.ShortWithdrawalShare, 0 ), - msg.sender, + _destination, shortWithdrawalShares ); shortWithdrawalSharesOutstanding += shortWithdrawalShares; // Withdraw the shares from the yield source // TODO - Good destination support. - (uint256 baseOutput, ) = withdraw(shareProceeds, msg.sender); + (uint256 baseOutput, ) = withdraw(shareProceeds, _destination); // Enforce min user outputs if (_minOutput > baseOutput) revert Errors.OutputLimit(); return (baseOutput, longWithdrawalShares, shortWithdrawalShares); @@ -335,11 +339,13 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @param _longWithdrawalShares The long withdrawal shares to redeem. /// @param _shortWithdrawalShares The short withdrawal shares to redeem. /// @param _minOutput The minimum amount of base the LP expects to receive. + /// @param _destination The address which receive the withdraw proceeds /// @return _proceeds The amount of base the LP received. function redeemWithdrawalShares( uint256 _longWithdrawalShares, uint256 _shortWithdrawalShares, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256 _proceeds) { uint256 baseProceeds = 0; @@ -369,7 +375,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Withdraw the funds released by redeeming the withdrawal shares. // TODO: Better destination support. uint256 shareProceeds = baseProceeds.divDown(sharePrice); - (_proceeds, ) = withdraw(shareProceeds, msg.sender); + (_proceeds, ) = withdraw(shareProceeds, _destination); // Enforce min user outputs if (_minOutput > _proceeds) revert Errors.OutputLimit(); @@ -418,10 +424,12 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @notice Opens a long position. /// @param _baseAmount The amount of base to use when trading. /// @param _minOutput The minium number of bonds to receive. + /// @param _destination The address which will receive the bonds /// @return The number of bonds the user received function openLong( uint256 _baseAmount, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256) { if (_baseAmount == 0) { revert Errors.ZeroAmount(); @@ -501,7 +509,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Mint the bonds to the trader with an ID of the maturity time. _mint( AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), - msg.sender, + _destination, bondProceeds ); return (bondProceeds); @@ -511,11 +519,13 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. /// @param _minOutput The minimum base the user should receive from this trade + /// @param _destination The address which will receive the proceeds of this sale /// @return The amount of underlying the user receives. function closeLong( uint256 _maturityTime, uint256 _bondAmount, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256) { if (_bondAmount == 0) { revert Errors.ZeroAmount(); @@ -557,20 +567,22 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { FixedPointMath.ONE_18, timeStretch ); - (uint256 _curveFee, uint256 _flatFee) = HyperdriveMath - .calculateFeesOutGivenIn( - _bondAmount, // amountIn - // normalizedTimeRemaining, when opening a position, the full time is remaining - FixedPointMath.ONE_18, - spotPrice, - sharePrice, - curveFee, - flatFee, - false // isBaseIn - ); - // This is a bond in / base out where the bonds are fixed, so we subtract from the base - // out. - shareProceeds -= _curveFee + _flatFee; + { + (uint256 _curveFee, uint256 _flatFee) = HyperdriveMath + .calculateFeesOutGivenIn( + _bondAmount, // amountIn + // normalizedTimeRemaining, when opening a position, the full time is remaining + FixedPointMath.ONE_18, + spotPrice, + sharePrice, + curveFee, + flatFee, + false // isBaseIn + ); + // This is a bond in / base out where the bonds are fixed, so we subtract from the base + // out. + shareProceeds -= _curveFee + _flatFee; + } // If the position hasn't matured, apply the accounting updates that // result from closing the long to the reserves and pay out the @@ -591,8 +603,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { } // Withdraw the profit to the trader. - // TODO: Better destination support. - (uint256 baseProceeds, ) = withdraw(shareProceeds, msg.sender); + (uint256 baseProceeds, ) = withdraw(shareProceeds, _destination); // Enforce min user outputs if (_minOutput > baseProceeds) revert Errors.OutputLimit(); @@ -605,10 +616,12 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @notice Opens a short position. /// @param _bondAmount The amount of bonds to short. /// @param _maxDeposit The most the user expects to deposit for this trade + /// @param _destination The address which gets credited with share tokens /// @return The amount the user deposited for this trade function openShort( uint256 _bondAmount, - uint256 _maxDeposit + uint256 _maxDeposit, + address _destination ) external returns (uint256) { if (_bondAmount == 0) { revert Errors.ZeroAmount(); @@ -646,13 +659,16 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // // interest = (c_1 - c_0) * (dy / c_0) // = (c_1 / c_0 - 1) * dy - uint256 owedInterest = (sharePrice.divDown(openSharePrice) - - FixedPointMath.ONE_18).mulDown(_bondAmount); - uint256 baseProceeds = shareProceeds.mulDown(sharePrice); - uint256 userDeposit = (_bondAmount - baseProceeds) + owedInterest; - // Enforce min user outputs - if (_maxDeposit < userDeposit) revert Errors.OutputLimit(); - deposit(userDeposit); // max_loss + interest + uint256 userDeposit; + { + uint256 owedInterest = (sharePrice.divDown(openSharePrice) - + FixedPointMath.ONE_18).mulDown(_bondAmount); + uint256 baseProceeds = shareProceeds.mulDown(sharePrice); + userDeposit = (_bondAmount - baseProceeds) + owedInterest; + // Enforce min user outputs + if (_maxDeposit < userDeposit) revert Errors.OutputLimit(); + deposit(userDeposit); // max_loss + interest + } // Apply the trading deltas to the reserves and increase the bond buffer // by the amount of bonds that were shorted. We don't need to add the @@ -672,7 +688,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // current share price and the maturity time of the shorts. _mint( AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, maturityTime), - msg.sender, + _destination, _bondAmount ); @@ -683,11 +699,13 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of shorts to close. /// @param _minOutput The minimum output of this trade. + /// @param _destination The address which gets the proceeds from closing this short /// @return The amount of base tokens produced by closing this short function closeShort( uint256 _maturityTime, uint256 _bondAmount, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256) { if (_bondAmount == 0) { revert Errors.ZeroAmount(); @@ -759,7 +777,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { sharePrice ); // TODO - Better destination support - (uint256 baseProceeds, ) = withdraw(shortProceeds, msg.sender); + (uint256 baseProceeds, ) = withdraw(shortProceeds, _destination); // Enforce min user outputs if (baseProceeds < _minOutput) revert Errors.OutputLimit(); diff --git a/contracts/interfaces/IHyperdrive.sol b/contracts/interfaces/IHyperdrive.sol index bbb67334e..2899c84ec 100644 --- a/contracts/interfaces/IHyperdrive.sol +++ b/contracts/interfaces/IHyperdrive.sol @@ -8,33 +8,39 @@ interface IHyperdrive is IMultiToken { function addLiquidity( uint256 _contribution, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256); function removeLiquidity( uint256 _shares, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256, uint256, uint256); function openLong( uint256 _baseAmount, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256); function closeLong( uint256 _maturityTime, uint256 _bondAmount, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256); function openShort( uint256 _bondAmount, - uint256 _maxDeposit + uint256 _maxDeposit, + address _destination ) external returns (uint256); function closeShort( uint256 _maturityTime, uint256 _bondAmount, - uint256 _minOutput + uint256 _minOutput, + address _destination ) external returns (uint256); } diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 38a1f6962..442acbd3f 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -124,7 +124,7 @@ contract HyperdriveTest is Test { vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.ZeroAmount.selector); - hyperdrive.addLiquidity(0, 0); + hyperdrive.addLiquidity(0, 0, bob); } /// openLong /// @@ -140,7 +140,7 @@ contract HyperdriveTest is Test { vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.ZeroAmount.selector); - hyperdrive.openLong(0, 0); + hyperdrive.openLong(0, 0, bob); } function test_open_long_extreme_amount() external { @@ -157,7 +157,7 @@ contract HyperdriveTest is Test { baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); vm.expectRevert(stdError.arithmeticError); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); } function test_open_long() external { @@ -181,7 +181,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); // Verify the base transfers. assertEq(baseToken.balanceOf(bob), 0); @@ -246,7 +246,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); // Attempt to close zero longs. This should fail. vm.stopPrank(); @@ -254,7 +254,7 @@ contract HyperdriveTest is Test { vm.expectRevert(Errors.ZeroAmount.selector); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; - hyperdrive.closeLong(maturityTime, 0, 0); + hyperdrive.closeLong(maturityTime, 0, 0, bob); } function test_close_long_invalid_amount() external { @@ -270,7 +270,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); // Attempt to close too many longs. This should fail. vm.stopPrank(); @@ -282,7 +282,7 @@ contract HyperdriveTest is Test { bob ); vm.expectRevert(stdError.arithmeticError); - hyperdrive.closeLong(maturityTime, bondAmount + 1, 0); + hyperdrive.closeLong(maturityTime, bondAmount + 1, 0, bob); } function test_close_long_invalid_timestamp() external { @@ -298,13 +298,13 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); // Attempt to use a timestamp greater than the maximum range. vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.InvalidTimestamp.selector); - hyperdrive.closeLong(uint256(type(uint248).max) + 1, 1, 0); + hyperdrive.closeLong(uint256(type(uint248).max) + 1, 1, 0, bob); } function test_close_long_immediately() external { @@ -320,7 +320,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); // Get the reserves before closing the long. PoolInfo memory poolInfoBefore = getPoolInfo(); @@ -334,7 +334,7 @@ contract HyperdriveTest is Test { AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), bob ); - hyperdrive.closeLong(maturityTime, bondAmount, 0); + hyperdrive.closeLong(maturityTime, bondAmount, 0, bob); // TODO: Bob receives more base than he started with. Fees should take // care of this, but this should be investigating nonetheless. @@ -390,7 +390,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount, 0); + hyperdrive.openLong(baseAmount, 0, bob); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; @@ -407,7 +407,7 @@ contract HyperdriveTest is Test { AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), bob ); - hyperdrive.closeLong(maturityTime, bondAmount, 0); + hyperdrive.closeLong(maturityTime, bondAmount, 0, bob); // TODO: Bob receives more base than the bond amount. It appears that // the yield space implementation returns a positive value even when @@ -459,7 +459,7 @@ contract HyperdriveTest is Test { vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.ZeroAmount.selector); - hyperdrive.openShort(0, type(uint256).max); + hyperdrive.openShort(0, type(uint256).max, bob); } function test_open_short_extreme_amount() external { @@ -476,7 +476,7 @@ contract HyperdriveTest is Test { baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); vm.expectRevert(Errors.FixedPointMath_SubOverflow.selector); - hyperdrive.openShort(baseAmount * 2, type(uint256).max); + hyperdrive.openShort(baseAmount * 2, type(uint256).max, bob); } function test_open_short() external { @@ -495,7 +495,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount, type(uint256).max); + hyperdrive.openShort(bondAmount, type(uint256).max, bob); // Verify that Hyperdrive received the max loss and that Bob received // the short tokens. @@ -568,7 +568,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount, type(uint256).max); + hyperdrive.openShort(bondAmount, type(uint256).max, bob); // Attempt to close zero shorts. This should fail. vm.stopPrank(); @@ -576,7 +576,7 @@ contract HyperdriveTest is Test { vm.expectRevert(Errors.ZeroAmount.selector); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; - hyperdrive.closeShort(maturityTime, 0, 0); + hyperdrive.closeShort(maturityTime, 0, 0, bob); } function test_close_short_invalid_amount() external { @@ -592,7 +592,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount, type(uint256).max); + hyperdrive.openShort(bondAmount, type(uint256).max, bob); // Attempt to close too many shorts. This should fail. vm.stopPrank(); @@ -600,7 +600,7 @@ contract HyperdriveTest is Test { uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; vm.expectRevert(stdError.arithmeticError); - hyperdrive.closeShort(maturityTime, bondAmount + 1, 0); + hyperdrive.closeShort(maturityTime, bondAmount + 1, 0, bob); } function test_close_short_invalid_timestamp() external { @@ -616,13 +616,13 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount, type(uint256).max); + hyperdrive.openShort(bondAmount, type(uint256).max, bob); // Attempt to use a timestamp greater than the maximum range. vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.InvalidTimestamp.selector); - hyperdrive.closeShort(uint256(type(uint248).max) + 1, 1, 0); + hyperdrive.closeShort(uint256(type(uint248).max) + 1, 1, 0, bob); } function test_close_short_immediately() external { @@ -638,7 +638,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount, type(uint256).max); + hyperdrive.openShort(bondAmount, type(uint256).max, bob); // Get the reserves before closing the long. PoolInfo memory poolInfoBefore = getPoolInfo(); @@ -648,7 +648,7 @@ contract HyperdriveTest is Test { vm.startPrank(bob); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; - hyperdrive.closeShort(maturityTime, bondAmount, 0); + hyperdrive.closeShort(maturityTime, bondAmount, 0, bob); // TODO: Bob receives more base than he started with. Fees should take // care of this, but this should be investigating nonetheless. @@ -708,7 +708,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount, type(uint256).max); + hyperdrive.openShort(bondAmount, type(uint256).max, bob); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; @@ -724,7 +724,7 @@ contract HyperdriveTest is Test { // Redeem the bonds vm.stopPrank(); vm.startPrank(bob); - hyperdrive.closeShort(maturityTime, bondAmount, 0); + hyperdrive.closeShort(maturityTime, bondAmount, 0, bob); // TODO: Investigate this more to see if there are any irregularities // like there are with the long redemption test.