Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 52 additions & 65 deletions src/HookBeaconProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,77 +16,64 @@ import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
/// This is an extension of the OpenZeppelin beacon proxy, however differs in that it is initializeable, which means
/// it is usable with Create2.
contract HookBeaconProxy is Proxy, ERC1967Upgrade {
/// @dev The constructor is empty in this case because the proxy is initializeable
constructor() {}
/// @dev The constructor is empty in this case because the proxy is initializeable
constructor() {}

bytes32 constant _INITIALIZED_SLOT =
bytes32(uint256(keccak256("initializeable.beacon.version")) - 1);
bytes32 constant _INITIALIZING_SLOT =
bytes32(uint256(keccak256("initializeable.beacon.initializing")) - 1);
bytes32 constant _INITIALIZED_SLOT = bytes32(uint256(keccak256("initializeable.beacon.version")) - 1);
bytes32 constant _INITIALIZING_SLOT = bytes32(uint256(keccak256("initializeable.beacon.initializing")) - 1);

///
/// @dev Triggered when the contract has been initialized or reinitialized.
///
event Initialized(uint8 version);
///
/// @dev Triggered when the contract has been initialized or reinitialized.
///
event Initialized(uint8 version);

/// @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
/// `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`.
modifier initializer() {
bool isTopLevelCall = _setInitializedVersion(1);
if (isTopLevelCall) {
StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = true;
/// @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
/// `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`.
modifier initializer() {
bool isTopLevelCall = _setInitializedVersion(1);
if (isTopLevelCall) {
StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = true;
}
_;
if (isTopLevelCall) {
StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = false;
emit Initialized(1);
}
}
_;
if (isTopLevelCall) {
StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = false;
emit Initialized(1);
}
}

function _setInitializedVersion(uint8 version) private returns (bool) {
// If the contract is initializing we ignore whether _initialized is set in order to support multiple
// inheritance patterns, but we only do this in the context of a constructor, and for the lowest level
// of initializers, because in other contexts the contract may have been reentered.
if (StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value) {
require(
version == 1 && !Address.isContract(address(this)),
"contract is already initialized"
);
return false;
} else {
require(
StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value < version,
"contract is already initialized"
);
StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value = version;
return true;
function _setInitializedVersion(uint8 version) private returns (bool) {
// If the contract is initializing we ignore whether _initialized is set in order to support multiple
// inheritance patterns, but we only do this in the context of a constructor, and for the lowest level
// of initializers, because in other contexts the contract may have been reentered.
if (StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value) {
require(version == 1 && !Address.isContract(address(this)), "contract is already initialized");
return false;
} else {
require(StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value < version, "contract is already initialized");
StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value = version;
return true;
}
}
}

/// @dev Initializes the proxy with `beacon`.
///
/// If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This
/// will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity
/// constructor.
///
/// Requirements:
///
///- `beacon` must be a contract with the interface {IBeacon}.
///
function initializeBeacon(address beacon, bytes memory data)
public
initializer
{
assert(
_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1)
);
_upgradeBeaconToAndCall(beacon, data, false);
}
/// @dev Initializes the proxy with `beacon`.
///
/// If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This
/// will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity
/// constructor.
///
/// Requirements:
///
///- `beacon` must be a contract with the interface {IBeacon}.
///
function initializeBeacon(address beacon, bytes memory data) public initializer {
assert(_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1));
_upgradeBeaconToAndCall(beacon, data, false);
}

///
/// @dev Returns the current implementation address of the associated beacon.
///
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}
///
/// @dev Returns the current implementation address of the associated beacon.
///
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}
}
164 changes: 68 additions & 96 deletions src/HookCoveredCallFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,100 +49,72 @@ import "@openzeppelin/contracts/utils/Create2.sol";
/// @dev See {IHookCoveredCallFactory}.
/// @dev The factory looks up certain roles by calling the {IHookProtocol} to verify
// that the caller is allowed to take certain actions
contract HookCoveredCallFactory is
PermissionConstants,
IHookCoveredCallFactory
{
/// @notice Registry of all of the active markets projects with supported call instruments
mapping(address => address) public override getCallInstrument;

/// @notice address of the beacon that contains the address of the current {IHookCoveredCall} implementation
address private immutable _beacon;

/// @notice the address of the protocol, which contains the rule
IHookProtocol private immutable _protocol;

/// @notice the address of an account that should automatically be approved to transfer the ERC-721 tokens
/// created by the {IHookCoveredCall} to represent instruments. This value is not used by the factory directly,
/// as this functionality is implemented by the {IHookCoveredCall}
address private immutable _preApprovedMarketplace;

/// @param hookProtocolAddress the address of the deployed {IHookProtocol} contract on this chain
/// @param beaconAddress the address of the deployed beacon pointing to the current covered call implementation
/// @param preApprovedMarketplace the address of an account approved to transfer instrument NFTs without owner approval
constructor(
address hookProtocolAddress,
address beaconAddress,
address preApprovedMarketplace
) {
require(
Address.isContract(hookProtocolAddress),
"hook protocol must be a contract"
);
require(
Address.isContract(beaconAddress),
"beacon address must be a contract"
);
require(
Address.isContract(preApprovedMarketplace),
"pre-approved marketplace must be a contract"
);
_beacon = beaconAddress;
_protocol = IHookProtocol(hookProtocolAddress);
_preApprovedMarketplace = preApprovedMarketplace;
}

/// @dev See {IHookCoveredCallFactory-makeCallInstrument}.
/// @dev Only holders of the ALLOWLISTER_ROLE on the {IHookProtocol} can create these addresses.
function makeCallInstrument(address assetAddress) external returns (address) {
require(
getCallInstrument[assetAddress] == address(0),
"makeCallInstrument-a call instrument already exists"
);
// make sure new instruments created by admins or the role
// has been burned
require(
_protocol.hasRole(ALLOWLISTER_ROLE, msg.sender) ||
_protocol.hasRole(ALLOWLISTER_ROLE, address(0)),
"makeCallInstrument-Only admins can make instruments"
);

IInitializeableBeacon bp = IInitializeableBeacon(
Create2.deploy(
0,
_callInstrumentSalt(assetAddress),
type(HookBeaconProxy).creationCode
)
);

bp.initializeBeacon(
_beacon,
/// This is the ABI encoded initializer on the IHookERC721Vault.sol
abi.encodeWithSignature(
"initialize(address,address,address,address)",
_protocol,
assetAddress,
_protocol.vaultContract(),
_preApprovedMarketplace
)
);

// Persist the call instrument onto the hook protocol
getCallInstrument[assetAddress] = address(bp);

emit CoveredCallInstrumentCreated(assetAddress, address(bp));

return address(bp);
}

/// @dev generate a consistent create2 salt to be used when deploying a
/// call instrument
/// @param underlyingAddress the account for the call option salt
function _callInstrumentSalt(address underlyingAddress)
internal
pure
returns (bytes32)
{
return keccak256(abi.encode(underlyingAddress));
}
contract HookCoveredCallFactory is PermissionConstants, IHookCoveredCallFactory {
/// @notice Registry of all of the active markets projects with supported call instruments
mapping(address => address) public override getCallInstrument;

/// @notice address of the beacon that contains the address of the current {IHookCoveredCall} implementation
address private immutable _beacon;

/// @notice the address of the protocol, which contains the rule
IHookProtocol private immutable _protocol;

/// @notice the address of an account that should automatically be approved to transfer the ERC-721 tokens
/// created by the {IHookCoveredCall} to represent instruments. This value is not used by the factory directly,
/// as this functionality is implemented by the {IHookCoveredCall}
address private immutable _preApprovedMarketplace;

/// @param hookProtocolAddress the address of the deployed {IHookProtocol} contract on this chain
/// @param beaconAddress the address of the deployed beacon pointing to the current covered call implementation
/// @param preApprovedMarketplace the address of an account approved to transfer instrument NFTs without owner approval
constructor(address hookProtocolAddress, address beaconAddress, address preApprovedMarketplace) {
require(Address.isContract(hookProtocolAddress), "hook protocol must be a contract");
require(Address.isContract(beaconAddress), "beacon address must be a contract");
require(Address.isContract(preApprovedMarketplace), "pre-approved marketplace must be a contract");
_beacon = beaconAddress;
_protocol = IHookProtocol(hookProtocolAddress);
_preApprovedMarketplace = preApprovedMarketplace;
}

/// @dev See {IHookCoveredCallFactory-makeCallInstrument}.
/// @dev Only holders of the ALLOWLISTER_ROLE on the {IHookProtocol} can create these addresses.
function makeCallInstrument(address assetAddress) external returns (address) {
require(getCallInstrument[assetAddress] == address(0), "makeCallInstrument-a call instrument already exists");
// make sure new instruments created by admins or the role
// has been burned
require(
_protocol.hasRole(ALLOWLISTER_ROLE, msg.sender) || _protocol.hasRole(ALLOWLISTER_ROLE, address(0)),
"makeCallInstrument-Only admins can make instruments"
);

IInitializeableBeacon bp = IInitializeableBeacon(
Create2.deploy(0, _callInstrumentSalt(assetAddress), type(HookBeaconProxy).creationCode)
);

bp.initializeBeacon(
_beacon,
/// This is the ABI encoded initializer on the IHookERC721Vault.sol
abi.encodeWithSignature(
"initialize(address,address,address,address)",
_protocol,
assetAddress,
_protocol.vaultContract(),
_preApprovedMarketplace
)
);

// Persist the call instrument onto the hook protocol
getCallInstrument[assetAddress] = address(bp);

emit CoveredCallInstrumentCreated(assetAddress, address(bp));

return address(bp);
}

/// @dev generate a consistent create2 salt to be used when deploying a
/// call instrument
/// @param underlyingAddress the account for the call option salt
function _callInstrumentSalt(address underlyingAddress) internal pure returns (bytes32) {
return keccak256(abi.encode(underlyingAddress));
}
}
Loading