Skip to content

Commit

Permalink
Merge branch 'main' into remove_extra_return_param_from_calc_fees
Browse files Browse the repository at this point in the history
  • Loading branch information
wakamex authored Nov 15, 2023
2 parents 2e146a4 + c9d52c1 commit 962af42
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 68 deletions.
1 change: 1 addition & 0 deletions contracts/src/instances/ERC4626Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ abstract contract ERC4626Base is HyperdriveBase {
);

// Deposit the base into the yield source.
ERC20(address(_baseToken)).safeApprove(address(_pool), _amount);
sharesMinted = _pool.deposit(_amount, address(this));
sharePrice = _pricePerShare();
} else {
Expand Down
9 changes: 5 additions & 4 deletions contracts/src/instances/ERC4626Hyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.19;

import { ERC20 } from "solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { Hyperdrive } from "../external/Hyperdrive.sol";
import { IERC20 } from "../interfaces/IERC20.sol";
import { IERC4626 } from "../interfaces/IERC4626.sol";
Expand All @@ -18,6 +19,7 @@ import { ERC4626Base } from "./ERC4626Base.sol";
/// particular legal or regulatory significance.
contract ERC4626Hyperdrive is Hyperdrive, ERC4626Base {
using FixedPointMath for uint256;
using SafeTransferLib for ERC20;

/// @notice Instantiates Hyperdrive with a ERC4626 vault as the yield source.
/// @param _config The configuration of the Hyperdrive pool.
Expand Down Expand Up @@ -49,10 +51,9 @@ contract ERC4626Hyperdrive is Hyperdrive, ERC4626Base {
revert IHyperdrive.InvalidBaseToken();
}

// Set immutables and prepare for deposits by setting immutables
if (!_config.baseToken.approve(address(_pool), type(uint256).max)) {
revert IHyperdrive.ApprovalFailed();
}
// Approve the base token with 1 wei. This ensures that all of the
// subsequent approvals will be writing to a dirty storage slot.
ERC20(address(_config.baseToken)).safeApprove(address(_pool), 1);

// Set the sweep targets. The base and pool tokens can't be set as sweep
// targets to prevent governance from rugging the pool.
Expand Down
5 changes: 3 additions & 2 deletions contracts/src/interfaces/IHyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,10 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken {
/// #############################
error InvalidForwarderAddress();

/// #####################
/// ###################
/// ### BondWrapper ###
/// #####################
/// ###################
error InvalidRecipient(address recipient);
error AlreadyClosed();
error BondMatured();
error BondNotMatured();
Expand Down
131 changes: 79 additions & 52 deletions contracts/src/token/BondWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ import { AssetId } from "../libraries/AssetId.sol";
contract BondWrapper is ERC20 {
using SafeTransferLib for ERC20;

// The multitoken of the bond
/// @notice The multitoken of the bond.
IHyperdrive public immutable hyperdrive;
// The underlying token from the bond

/// @notice The underlying token from the bond.
IERC20 public immutable token;
// The basis points [ie out of 10000] which will be minted for a bond deposit

/// @notice The basis points (i.e. out of 10000) which will be minted for a
/// bond deposit.
uint256 public immutable mintPercent;

// Store the user deposits as a mapping from user address -> asset id -> amount
/// @notice Store the user deposits as a mapping from user address to asset
/// ID to amount.
mapping(address user => mapping(uint256 assetId => uint256 amount))
public deposits;

Expand All @@ -44,40 +48,49 @@ contract BondWrapper is ERC20 {
revert IHyperdrive.MintPercentTooHigh();
}

// By setting these addresses to the max uint256, attempting to execute
// a transfer to either of them will revert. This is a gas efficient way
// to prevent a common user mistake where they transfer to the token
// address. These values are not considered 'real' tokens and so are not
// included in 'total supply' which only contains minted tokens.
// WARN - Never allow allowances to be set for these addresses.
balanceOf[address(0)] = type(uint256).max;
balanceOf[address(this)] = type(uint256).max;

// Set the immutables
hyperdrive = _hyperdrive;
token = _token;
mintPercent = _mintPercent;
}

/// @notice Transfers bonds from the user and then mints erc20 for the mintable percent.
/// @param maturityTime The bond's expiry time
/// @param amount The amount of bonds to mint
/// @param destination The address which gets credited with these funds
/// @notice Transfers bonds from the user to the recipient.
/// @param to The recipient of the bonds.
/// @param amount The amount of bonds to transfer.
/// @return True if the transfer succeeded.
function transfer(
address to,
uint256 amount
) public override returns (bool) {
// Ensure that the recipient isn't the zero address or this address.
if (to == address(0) || to == address(this)) {
revert IHyperdrive.InvalidRecipient(to);
}

// Complete the transfer.
return super.transfer(to, amount);
}

/// @notice Transfers bonds from the user and then mints erc20 for the
/// mintable percent.
/// @param maturityTime The bond's expiry time.
/// @param amount The amount of bonds to mint.
/// @param destination The address which gets credited with these funds.
function mint(
uint256 maturityTime,
uint256 amount,
address destination
) external {
// Must not be matured
// Must not be matured.
if (maturityTime <= block.timestamp) revert IHyperdrive.BondMatured();

// Encode the asset ID
// Encode the asset ID.
uint256 assetId = AssetId.encodeAssetId(
AssetId.AssetIdPrefix.Long,
maturityTime
);

// Transfer from the user
// Transfer from the user.
hyperdrive.transferFrom(assetId, msg.sender, address(this), amount);

// Mint them the tokens for their deposit
Expand All @@ -88,14 +101,16 @@ contract BondWrapper is ERC20 {
deposits[destination][assetId] += amount;
}

/// @notice Closes a user account by selling the bond and then transferring the delta value of that
/// sale vs the erc20 tokens minted by its deposit. Optionally also burns the ERC20 wrapper
/// from the user, if enabled it will transfer both the delta of sale value and the value of
/// the burned token.
/// @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
/// @notice Closes a user account by selling the bond and then transferring
/// the delta value of that sale vs the erc20 tokens minted by its
/// deposit. Optionally also burns the ERC20 wrapper from the user,
/// if enabled it will transfer both the delta of sale value and the
/// value of the burned token.
/// @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.
/// @param minOutput The min amount the user expects transferred to them.
/// @param extraData Extra data to pass to the yield source.
function close(
Expand All @@ -106,15 +121,15 @@ contract BondWrapper is ERC20 {
uint256 minOutput,
bytes memory extraData
) external {
// Encode the asset ID
// Encode the asset ID.
uint256 assetId = AssetId.encodeAssetId(
AssetId.AssetIdPrefix.Long,
maturityTime
);

uint256 receivedAmount;
if (maturityTime > block.timestamp) {
// Close the bond [selling if earlier than the expiration]
// Close the bond (selling if earlier than the expiration).
receivedAmount = hyperdrive.closeLong(
maturityTime,
amount,
Expand All @@ -126,26 +141,29 @@ contract BondWrapper is ERC20 {
})
);
} else {
// Sell all assets
// Sell all assets.
sweep(maturityTime, extraData);
// Sweep guarantees 1 to 1 conversion so the user gets exactly the amount they are closing

// Sweep guarantees 1 to 1 conversion so the user gets exactly the
// amount they are closing.
receivedAmount = amount;
}
// Update the user balances

// Update the user balances.
deposits[msg.sender][assetId] -= amount;

// Close the user position
// We require that this won't make the position unbacked
// Close the user position. We require that this won't make the position
// unbacked.
uint256 mintedFromBonds = (amount * mintPercent) / 10_000;

if (receivedAmount < mintedFromBonds)
if (receivedAmount < mintedFromBonds) {
revert IHyperdrive.InsufficientPrice();
}

// The user gets at least the interest implied from
// The user gets at least the interest implied from the mint percentage.
uint256 userFunds = receivedAmount - mintedFromBonds;

// If the user would also like to burn the erc20 from their wallet
if (andBurn) {
// If requested, burn the user's bonds and increase the user's funds.
if (andBurn && mintedFromBonds > 0) {
_burn(msg.sender, mintedFromBonds);
userFunds += mintedFromBonds;
}
Expand All @@ -154,22 +172,28 @@ contract BondWrapper is ERC20 {
if (userFunds < minOutput) revert IHyperdrive.OutputLimit();

// Transfer the released funds to the user
ERC20(address(token)).safeTransfer(destination, userFunds);
if (userFunds > 0) {
ERC20(address(token)).safeTransfer(destination, userFunds);
}
}

/// @notice Sells all assets from the contract if they are matured, has no affect if
/// the contract has no assets from a timestamp
/// @param maturityTime The maturity time of the asset to sell
/// @notice Sells all assets from the contract if they are matured, has no
/// effect if the contract has no assets from a timestamp.
/// @param maturityTime The maturity time of the asset to sell.
/// @param extraData Extra data to pass to the yield source.
function sweep(uint256 maturityTime, bytes memory extraData) public {
// Require only sweeping after maturity
if (maturityTime > block.timestamp) revert IHyperdrive.BondNotMatured();
// Load the balance of this contract
// Ensure that the bonds haven't matured yet.
if (maturityTime > block.timestamp) {
revert IHyperdrive.BondNotMatured();
}

// Load the balance of this contract.
uint256 assetId = AssetId.encodeAssetId(
AssetId.AssetIdPrefix.Long,
maturityTime
);
uint256 balance = hyperdrive.balanceOf(assetId, address(this));

// Only close if we have something to close
if (balance != 0) {
// Since we're closing the entire position, the output can be ignored.
Expand All @@ -186,18 +210,21 @@ contract BondWrapper is ERC20 {
}
}

/// @notice Burns a caller's erc20 and transfers the result from the contract's token balance.
/// @notice Burns a caller's erc20 and transfers the result from the
/// contract's token balance.
/// @param amount The amount of erc20 wrapper to burn.
function redeem(uint256 amount) public {
// Simply burn from the user and send funds from the contract balance
// Simply burn from the user and send funds from the contract balance.
_burn(msg.sender, amount);

// Transfer the released funds to the user
// Transfer the released funds to the user.
ERC20(address(token)).safeTransfer(msg.sender, amount);
}

/// @notice Calls both force close and redeem to enable easy liquidation of a user account
/// @param maturityTimes Maturity times which the caller would like to sweep before redeeming
/// @notice Calls both force close and redeem to enable easy liquidation of
/// a user account.
/// @param maturityTimes Maturity times which the caller would like to
/// sweep before redeeming.
/// @param amount The amount of erc20 wrapper to burn.
/// @param extraDatas Extra data to pass to the yield source.
function sweepAndRedeem(
Expand Down
16 changes: 9 additions & 7 deletions test/combinatorial/BondWrapper.close.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -306,18 +306,20 @@ contract BondWrapper_close is CombinatorialTest {
}

// A users wrapped long tokens should be burned if they specify to do so
if (testCase.andBurn) {
if (testCase.andBurn && testCase.mintedFromBonds > 0) {
vm.expectEmit(true, true, true, true);
emit Transfer(testCase.user, address(0), testCase.mintedFromBonds);
}

// Some amount of baseToken should be sent to the user
vm.expectEmit(true, true, true, true);
emit Transfer(
address(bondWrapper),
testCase.destination,
testCase.userFunds
);
if (testCase.userFunds > 0) {
vm.expectEmit(true, true, true, true);
emit Transfer(
address(bondWrapper),
testCase.destination,
testCase.userFunds
);
}

// Caching balances prior to executing transaction for differentials
uint256 userDeposit = bondWrapper.deposits(
Expand Down
30 changes: 27 additions & 3 deletions test/units/BondWrapper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,31 @@ contract BondWrapperTest is BaseTest {
vm.stopPrank();
}

function test_BondWrapperRedeem() public {
function test_FailsToTransferToInvalidRecipient() external {
uint256 amount = 1e18;
bondWrapper.mint(alice, amount);

// Ensure that Alice can't transfer to the zero address.
vm.startPrank(alice);
vm.expectRevert(
abi.encodeWithSelector(
IHyperdrive.InvalidRecipient.selector,
address(0)
)
);
bondWrapper.transfer(address(0), amount);

// Ensure that Alice can't transfer to the bond wrapper address.
vm.expectRevert(
abi.encodeWithSelector(
IHyperdrive.InvalidRecipient.selector,
address(bondWrapper)
)
);
bondWrapper.transfer(address(bondWrapper), amount);
}

function test_BondWrapperRedeem() external {
// Ensure that the bondWrapper contract has been approved by the user
vm.startPrank(alice);
multiToken.setApprovalForAll(address(bondWrapper), true);
Expand All @@ -113,7 +137,7 @@ contract BondWrapperTest is BaseTest {
assert(balance == 0);
}

function test_bond_wrapper_closeLimit() public {
function test_bond_wrapper_closeLimit() external {
// Ensure that the bondWrapper contract has been approved by the user
vm.startPrank(alice);
multiToken.setApprovalForAll(address(bondWrapper), true);
Expand Down Expand Up @@ -155,7 +179,7 @@ contract BondWrapperTest is BaseTest {
);
}

function test_sweepAndRedeem() public {
function test_sweepAndRedeem() external {
// Alice mints some BondWrapper tokens.
vm.startPrank(alice);
uint256 balance = bondWrapper.balanceOf(alice);
Expand Down

0 comments on commit 962af42

Please sign in to comment.