272 lines
8.7 KiB
Solidity
272 lines
8.7 KiB
Solidity
|
// SPDX-License-Identifier: CC0.0 OR Apache-2.0
|
||
|
// Author: Zainan Victor Zhou <zzn-ercref@zzn.im>
|
||
|
// See a full runnable hardhat project in https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5453
|
||
|
pragma solidity ^0.8.9;
|
||
|
|
||
|
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
|
||
|
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
||
|
|
||
|
import "./IERC5453.sol";
|
||
|
|
||
|
abstract contract AERC5453Endorsible is EIP712,
|
||
|
IERC5453EndorsementCore, IERC5453EndorsementDigest, IERC5453EndorsementDataTypeA, IERC5453EndorsementDataTypeB {
|
||
|
uint256 private threshold;
|
||
|
uint256 private currentNonce = 0;
|
||
|
bytes32 constant MAGIC_WORLD = keccak256("ERC5453-ENDORSEMENT"); // ASCII of "ENDORSED"
|
||
|
uint256 constant ERC5453_TYPE_A = 1;
|
||
|
uint256 constant ERC5453_TYPE_B = 2;
|
||
|
|
||
|
constructor(
|
||
|
string memory _name,
|
||
|
string memory _erc721Version
|
||
|
) EIP712(_name, _erc721Version) {}
|
||
|
|
||
|
function _validate(
|
||
|
bytes32 msgDigest,
|
||
|
SingleEndorsementData memory endersement
|
||
|
) internal virtual {
|
||
|
require(
|
||
|
endersement.sig.length == 65,
|
||
|
"AERC5453Endorsible: wrong signature length"
|
||
|
);
|
||
|
require(
|
||
|
SignatureChecker.isValidSignatureNow(
|
||
|
endersement.endorserAddress,
|
||
|
msgDigest,
|
||
|
endersement.sig
|
||
|
),
|
||
|
"AERC5453Endorsible: invalid signature"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function _extractEndorsers(
|
||
|
bytes32 digest,
|
||
|
GeneralExtensionDataStruct memory data
|
||
|
) internal virtual returns (address[] memory endorsers) {
|
||
|
require(
|
||
|
data.erc5453MagicWord == MAGIC_WORLD,
|
||
|
"AERC5453Endorsible: MagicWord not matched"
|
||
|
);
|
||
|
require(
|
||
|
data.validSince <= block.number,
|
||
|
"AERC5453Endorsible: Not valid yet"
|
||
|
); // TODO consider per-Endorser validSince
|
||
|
require(data.validBy >= block.number, "AERC5453Endorsible: Expired"); // TODO consider per-Endorser validBy
|
||
|
require(
|
||
|
currentNonce == data.nonce,
|
||
|
"AERC5453Endorsible: Nonce not matched"
|
||
|
); // TODO consider per-Endorser nonce or range of nonce
|
||
|
currentNonce += 1;
|
||
|
|
||
|
if (data.erc5453Type == ERC5453_TYPE_A) {
|
||
|
SingleEndorsementData memory endersement = abi.decode(
|
||
|
data.endorsementPayload,
|
||
|
(SingleEndorsementData)
|
||
|
);
|
||
|
endorsers = new address[](1);
|
||
|
endorsers[0] = endersement.endorserAddress;
|
||
|
_validate(digest, endersement);
|
||
|
} else if (data.erc5453Type == ERC5453_TYPE_B) {
|
||
|
SingleEndorsementData[] memory endorsements = abi.decode(
|
||
|
data.endorsementPayload,
|
||
|
(SingleEndorsementData[])
|
||
|
);
|
||
|
endorsers = new address[](endorsements.length);
|
||
|
for (uint256 i = 0; i < endorsements.length; ++i) {
|
||
|
endorsers[i] = endorsements[i].endorserAddress;
|
||
|
_validate(digest, endorsements[i]);
|
||
|
}
|
||
|
return endorsers;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function _decodeExtensionData(
|
||
|
bytes memory extensionData
|
||
|
) internal pure virtual returns (GeneralExtensionDataStruct memory) {
|
||
|
return abi.decode(extensionData, (GeneralExtensionDataStruct));
|
||
|
}
|
||
|
|
||
|
// Well, I know this is epensive. Let's improve it later.
|
||
|
function _noRepeat(address[] memory _owners) internal pure returns (bool) {
|
||
|
for (uint256 i = 0; i < _owners.length; i++) {
|
||
|
for (uint256 j = i + 1; j < _owners.length; j++) {
|
||
|
if (_owners[i] == _owners[j]) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function _isEndorsed(
|
||
|
bytes32 _functionParamStructHash,
|
||
|
bytes calldata _extraData
|
||
|
) internal returns (bool) {
|
||
|
GeneralExtensionDataStruct memory _data = _decodeExtensionData(
|
||
|
_extraData
|
||
|
);
|
||
|
bytes32 finalDigest = _computeValidityDigest(
|
||
|
_functionParamStructHash,
|
||
|
_data.validSince,
|
||
|
_data.validBy,
|
||
|
_data.nonce
|
||
|
);
|
||
|
|
||
|
address[] memory endorsers = _extractEndorsers(finalDigest, _data);
|
||
|
require(
|
||
|
endorsers.length >= threshold,
|
||
|
"AERC5453Endorsable: not enough endorsers"
|
||
|
);
|
||
|
require(_noRepeat(endorsers));
|
||
|
for (uint256 i = 0; i < endorsers.length; i++) {
|
||
|
require(
|
||
|
_isEligibleEndorser(endorsers[i]),
|
||
|
"AERC5453Endorsable: not eligible endorsers"
|
||
|
); // everyone must be a legit endorser
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function _isEligibleEndorser(
|
||
|
address /*_endorser*/
|
||
|
) internal view virtual returns (bool);
|
||
|
|
||
|
modifier onlyEndorsed(
|
||
|
bytes32 _functionParamStructHash,
|
||
|
bytes calldata _extensionData
|
||
|
) {
|
||
|
require(_isEndorsed(_functionParamStructHash, _extensionData));
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
function _computeValidityDigest(
|
||
|
bytes32 _functionParamStructHash,
|
||
|
uint256 _validSince,
|
||
|
uint256 _validBy,
|
||
|
uint256 _nonce
|
||
|
) internal view returns (bytes32) {
|
||
|
return
|
||
|
super._hashTypedDataV4(
|
||
|
keccak256(
|
||
|
abi.encode(
|
||
|
keccak256(
|
||
|
"ValidityBound(bytes32 functionParamStructHash,uint256 validSince,uint256 validBy,uint256 nonce)"
|
||
|
),
|
||
|
_functionParamStructHash,
|
||
|
_validSince,
|
||
|
_validBy,
|
||
|
_nonce
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function _computeFunctionParamHash(
|
||
|
string memory _functionStructure,
|
||
|
bytes memory _functionParamPacked
|
||
|
) internal pure returns (bytes32) {
|
||
|
bytes32 functionParamStructHash = keccak256(
|
||
|
abi.encodePacked(
|
||
|
keccak256(bytes(_functionStructure)),
|
||
|
_functionParamPacked
|
||
|
)
|
||
|
);
|
||
|
return functionParamStructHash;
|
||
|
}
|
||
|
|
||
|
function _setThreshold(uint256 _threshold) internal virtual {
|
||
|
threshold = _threshold;
|
||
|
}
|
||
|
|
||
|
function computeValidityDigest(
|
||
|
bytes32 _functionParamStructHash,
|
||
|
uint256 _validSince,
|
||
|
uint256 _validBy,
|
||
|
uint256 _nonce
|
||
|
) external view override returns (bytes32) {
|
||
|
return
|
||
|
_computeValidityDigest(
|
||
|
_functionParamStructHash,
|
||
|
_validSince,
|
||
|
_validBy,
|
||
|
_nonce
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function computeFunctionParamHash(
|
||
|
string memory _functionName,
|
||
|
bytes memory _functionParamPacked
|
||
|
) external pure override returns (bytes32) {
|
||
|
return
|
||
|
_computeFunctionParamHash(
|
||
|
_functionName,
|
||
|
_functionParamPacked
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function eip5453Nonce(address addr) external view override returns (uint256) {
|
||
|
require(address(this) == addr, "AERC5453Endorsable: not self");
|
||
|
return currentNonce;
|
||
|
}
|
||
|
|
||
|
function isEligibleEndorser(address _endorser)
|
||
|
external
|
||
|
view
|
||
|
override
|
||
|
returns (bool)
|
||
|
{
|
||
|
return _isEligibleEndorser(_endorser);
|
||
|
}
|
||
|
|
||
|
function computeExtensionDataTypeA(
|
||
|
uint256 nonce,
|
||
|
uint256 validSince,
|
||
|
uint256 validBy,
|
||
|
address endorserAddress,
|
||
|
bytes calldata sig
|
||
|
) external pure override returns (bytes memory) {
|
||
|
return
|
||
|
abi.encode(
|
||
|
GeneralExtensionDataStruct(
|
||
|
MAGIC_WORLD,
|
||
|
ERC5453_TYPE_A,
|
||
|
nonce,
|
||
|
validSince,
|
||
|
validBy,
|
||
|
abi.encode(SingleEndorsementData(endorserAddress, sig))
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function computeExtensionDataTypeB(
|
||
|
uint256 nonce,
|
||
|
uint256 validSince,
|
||
|
uint256 validBy,
|
||
|
address[] calldata endorserAddress,
|
||
|
bytes[] calldata sigs
|
||
|
) external pure override returns (bytes memory) {
|
||
|
require(endorserAddress.length == sigs.length);
|
||
|
SingleEndorsementData[]
|
||
|
memory endorsements = new SingleEndorsementData[](
|
||
|
endorserAddress.length
|
||
|
);
|
||
|
for (uint256 i = 0; i < endorserAddress.length; ++i) {
|
||
|
endorsements[i] = SingleEndorsementData(
|
||
|
endorserAddress[i],
|
||
|
sigs[i]
|
||
|
);
|
||
|
}
|
||
|
return
|
||
|
abi.encode(
|
||
|
GeneralExtensionDataStruct(
|
||
|
MAGIC_WORLD,
|
||
|
ERC5453_TYPE_B,
|
||
|
nonce,
|
||
|
validSince,
|
||
|
validBy,
|
||
|
abi.encode(endorsements)
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|