Created
July 29, 2023 10:40
-
-
Save CJ42/3c0a109fbbc30e422d4808499afa252e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: Apache-2.0 | |
pragma solidity ^0.8.5; | |
// interfaces | |
import { | |
IERC725Y | |
} from "@erc725/smart-contracts/contracts/interfaces/IERC725Y.sol"; | |
import {ILSP6KeyManager} from "./ILSP6KeyManager.sol"; | |
// modules | |
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; | |
import {ERC725Y} from "@erc725/smart-contracts/contracts/ERC725Y.sol"; | |
import { | |
OwnableUnset | |
} from "@erc725/smart-contracts/contracts/custom/OwnableUnset.sol"; | |
import {LSP6SetDataModule} from "./LSP6Modules/LSP6SetDataModule.sol"; | |
import {LSP6OwnershipModule} from "./LSP6Modules/LSP6OwnershipModule.sol"; | |
// librairies | |
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; | |
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | |
import {LSP6Utils} from "./LSP6Utils.sol"; | |
// constants | |
import { | |
_INTERFACEID_LSP6, | |
LSP6_VERSION, | |
_PERMISSION_SIGN | |
} from "./LSP6Constants.sol"; | |
import { | |
_INTERFACEID_ERC1271, | |
_ERC1271_MAGICVALUE, | |
_ERC1271_FAILVALUE | |
} from "../LSP0ERC725Account/LSP0Constants.sol"; | |
// errors | |
import { | |
BatchExecuteParamsLengthMismatch, | |
BatchExecuteRelayCallParamsLengthMismatch, | |
LSP6BatchExcessiveValueSent, | |
LSP6BatchInsufficientValueSent, | |
InvalidPayload, | |
InvalidRelayNonce, | |
NoPermissionsSet, | |
InvalidERC725Function, | |
CannotSendValueToSetData, | |
RelayCallBeforeStartTime, | |
RelayCallExpired, | |
InvalidLSP6Target | |
} from "./LSP6Errors.sol"; | |
/** | |
* @title LSP6 Implementation to manage a LSP7/8 Token through multiple controllers | |
* @author Jean Cavallera <CJ42> | |
*/ | |
contract LSP6TokenManager is | |
ILSP6KeyManager, | |
ERC165, | |
LSP6SetDataModule, | |
LSP6OwnershipModule | |
{ | |
using Address for *; | |
using ECDSA for *; | |
using LSP6Utils for *; | |
/** | |
* @dev address of the LSP7/8 Token contract this Key Manager controls | |
*/ | |
address private immutable _linkedToken; | |
mapping(address => mapping(uint256 => uint256)) internal _nonceStore; | |
constructor(address linkedToken_) { | |
if (linkedToken_ == address(0)) revert InvalidLSP6Target(); | |
_linkedToken = linkedToken_; | |
} | |
/** | |
* TODO: is it really necessary that the LSP6 interface inherits the ERC1271 interface? | |
* As in the context of a Key Manager linked to a token like here, does the Key Manager | |
* really need to implement `isValidSignature(...)` function? What would be the use case? | |
*/ | |
// function isValidSignature( | |
// bytes32 dataHash, | |
// bytes memory signature | |
// ) public view returns (bytes4 magicValue) { | |
// // if isValidSignature fail, the error is catched in returnedError | |
// (address recoveredAddress, ECDSA.RecoverError returnedError) = ECDSA | |
// .tryRecover(dataHash, signature); | |
// // if recovering throws an error, return the fail value | |
// if (returnedError != ECDSA.RecoverError.NoError) | |
// return _ERC1271_FAILVALUE; | |
// // if the address recovered has SIGN permission return the ERC1271 magic value, otherwise the fail value | |
// return ( | |
// ERC725Y(_linkedToken) | |
// .getPermissionsFor(recoveredAddress) | |
// .hasPermission(_PERMISSION_SIGN) | |
// ? _ERC1271_MAGICVALUE | |
// : _ERC1271_FAILVALUE | |
// ); | |
// } | |
function target() public view returns (address) { | |
return _linkedToken; | |
} | |
/** | |
* @dev See {IERC165-supportsInterface}. | |
*/ | |
function supportsInterface( | |
bytes4 interfaceId | |
) public view virtual override returns (bool) { | |
return | |
interfaceId == _INTERFACEID_LSP6 || | |
super.supportsInterface(interfaceId); | |
} | |
/** | |
* @inheritdoc ILSP6KeyManager | |
*/ | |
function getNonce( | |
address from, | |
uint128 channelId | |
) public view returns (uint256) { | |
uint256 nonceInChannel = _nonceStore[from][channelId]; | |
return (uint256(channelId) << 128) | nonceInChannel; | |
} | |
/** | |
* @inheritdoc ILSP6KeyManager | |
*/ | |
function execute( | |
bytes calldata payload | |
) external payable returns (bytes memory) { | |
return _execute(msg.value, payload); | |
} | |
/** | |
* @inheritdoc ILSP6KeyManager | |
*/ | |
function executeBatch( | |
uint256[] calldata values, | |
bytes[] calldata payloads | |
) external payable returns (bytes[] memory) { | |
if (values.length != payloads.length) { | |
revert BatchExecuteParamsLengthMismatch(); | |
} | |
bytes[] memory results = new bytes[](payloads.length); | |
uint256 totalValues; | |
for (uint256 ii = 0; ii < payloads.length; ) { | |
if ((totalValues += values[ii]) > msg.value) { | |
revert LSP6BatchInsufficientValueSent(totalValues, msg.value); | |
} | |
results[ii] = _execute(values[ii], payloads[ii]); | |
unchecked { | |
++ii; | |
} | |
} | |
if (totalValues < msg.value) { | |
revert LSP6BatchExcessiveValueSent(totalValues, msg.value); | |
} | |
return results; | |
} | |
/** | |
* @inheritdoc ILSP6KeyManager | |
*/ | |
function executeRelayCall( | |
bytes memory signature, | |
uint256 nonce, | |
uint256 validityTimestamps, | |
bytes calldata payload | |
) public payable virtual returns (bytes memory) { | |
return | |
_executeRelayCall( | |
signature, | |
nonce, | |
validityTimestamps, | |
msg.value, | |
payload | |
); | |
} | |
/** | |
* @inheritdoc ILSP6KeyManager | |
*/ | |
function executeRelayCallBatch( | |
bytes[] memory signatures, | |
uint256[] calldata nonces, | |
uint256[] calldata validityTimestamps, | |
uint256[] calldata values, | |
bytes[] calldata payloads | |
) public payable virtual returns (bytes[] memory) { | |
if ( | |
signatures.length != nonces.length || | |
nonces.length != validityTimestamps.length || | |
validityTimestamps.length != values.length || | |
values.length != payloads.length | |
) { | |
revert BatchExecuteRelayCallParamsLengthMismatch(); | |
} | |
bytes[] memory results = new bytes[](payloads.length); | |
uint256 totalValues; | |
for (uint256 ii = 0; ii < payloads.length; ) { | |
if ((totalValues += values[ii]) > msg.value) { | |
revert LSP6BatchInsufficientValueSent(totalValues, msg.value); | |
} | |
results[ii] = _executeRelayCall( | |
signatures[ii], | |
nonces[ii], | |
validityTimestamps[ii], | |
values[ii], | |
payloads[ii] | |
); | |
unchecked { | |
++ii; | |
} | |
} | |
if (totalValues < msg.value) { | |
revert LSP6BatchExcessiveValueSent(totalValues, msg.value); | |
} | |
return results; | |
} | |
function _execute( | |
uint256 msgValue, | |
bytes calldata payload | |
) internal virtual returns (bytes memory) { | |
if (payload.length < 4) { | |
revert InvalidPayload(payload); | |
} | |
_verifyPermissions(msg.sender, msgValue, payload); | |
emit VerifiedCall(msg.sender, msgValue, bytes4(payload)); | |
return _executePayload(msgValue, payload); | |
} | |
function _executeRelayCall( | |
bytes memory signature, | |
uint256 nonce, | |
uint256 validityTimestamps, | |
uint256 msgValue, | |
bytes calldata payload | |
) internal virtual returns (bytes memory) { | |
if (payload.length < 4) { | |
revert InvalidPayload(payload); | |
} | |
bytes memory encodedMessage = abi.encodePacked( | |
LSP6_VERSION, | |
block.chainid, | |
nonce, | |
validityTimestamps, | |
msgValue, | |
payload | |
); | |
address signer = address(this) | |
.toDataWithIntendedValidatorHash(encodedMessage) | |
.recover(signature); | |
if (!_isValidNonce(signer, nonce)) { | |
revert InvalidRelayNonce(signer, nonce, signature); | |
} | |
// increase nonce after successful verification | |
_nonceStore[signer][nonce >> 128]++; | |
if (validityTimestamps != 0) { | |
uint128 startingTimestamp = uint128(validityTimestamps >> 128); | |
uint128 endingTimestamp = uint128(validityTimestamps); | |
// solhint-disable not-rely-on-time | |
if (block.timestamp < startingTimestamp) { | |
revert RelayCallBeforeStartTime(); | |
} | |
if (block.timestamp > endingTimestamp) { | |
revert RelayCallExpired(); | |
} | |
} | |
_verifyPermissions(signer, msgValue, payload); | |
emit VerifiedCall(signer, msgValue, bytes4(payload)); | |
return _executePayload(msgValue, payload); | |
} | |
/** | |
* @notice execute the `payload` passed to `execute(...)` or `executeRelayCall(...)` | |
* @param payload the abi-encoded function call to execute on the target. | |
* @return bytes the result from calling the target with `payload` | |
*/ | |
function _executePayload( | |
uint256 msgValue, | |
bytes calldata payload | |
) internal virtual returns (bytes memory) { | |
(bool success, bytes memory returnData) = _linkedToken.call{ | |
value: msgValue, | |
gas: gasleft() | |
}(payload); | |
bytes memory result = success.verifyCallResult( | |
returnData, | |
"LSP6: failed executing payload" | |
); | |
return result.length != 0 ? abi.decode(result, (bytes)) : result; | |
} | |
/** | |
* @notice verify the nonce `_idx` for `_from` (obtained via `getNonce(...)`) | |
* @dev "idx" is a 256bits (unsigned) integer, where: | |
* - the 128 leftmost bits = channelId | |
* and - the 128 rightmost bits = nonce within the channel | |
* @param from caller address | |
* @param idx (channel id + nonce within the channel) | |
*/ | |
function _isValidNonce( | |
address from, | |
uint256 idx | |
) internal view virtual returns (bool) { | |
uint256 mask = ~uint128(0); | |
// Alternatively: | |
// uint256 mask = (1<<128)-1; | |
// uint256 mask = 0xffffffffffffffffffffffffffffffff; | |
return (idx & mask) == (_nonceStore[from][idx >> 128]); | |
} | |
/** | |
* @dev verify if the `from` address is allowed to execute the `payload` on the `target`. | |
* @param from either the caller of `execute(...)` or the signer of `executeRelayCall(...)`. | |
* @param payload the payload to execute on the `target`. | |
*/ | |
function _verifyPermissions( | |
address from, | |
uint256 msgValue, | |
bytes calldata payload | |
) internal view virtual { | |
bytes32 permissions = ERC725Y(_linkedToken).getPermissionsFor(from); | |
if (permissions == bytes32(0)) revert NoPermissionsSet(from); | |
bytes4 erc725Function = bytes4(payload); | |
// ERC725Y.setData(bytes32,bytes) | |
if (erc725Function == IERC725Y.setData.selector) { | |
if (msgValue != 0) revert CannotSendValueToSetData(); | |
(bytes32 inputKey, bytes memory inputValue) = abi.decode( | |
payload[4:], | |
(bytes32, bytes) | |
); | |
LSP6SetDataModule._verifyCanSetData( | |
_linkedToken, | |
from, | |
permissions, | |
inputKey, | |
inputValue | |
); | |
} else if (erc725Function == IERC725Y.setDataBatch.selector) { | |
if (msgValue != 0) revert CannotSendValueToSetData(); | |
(bytes32[] memory inputKeys, bytes[] memory inputValues) = abi | |
.decode(payload[4:], (bytes32[], bytes[])); | |
LSP6SetDataModule._verifyCanSetData( | |
_linkedToken, | |
from, | |
permissions, | |
inputKeys, | |
inputValues | |
); | |
} else if (erc725Function == OwnableUnset.transferOwnership.selector) { | |
LSP6OwnershipModule._verifyOwnershipPermissions(from, permissions); | |
} else { | |
revert InvalidERC725Function(erc725Function); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment