```
pragma solidity >=0.6.0 <0.7.0;
pragma experimental ABIEncoderV2;
// SPDX-License-Identifier: MIT
/**
* EPNS Communicator, as the name suggests, is more of a Communictation Layer
* between END USERS and EPNS Core Protocol.
* The Communicator Protocol is comparatively much simpler & involves basic
* details, specifically about the USERS of the Protocols
* Some imperative functionalities that the EPNS Communicator Protocol allows
* are Subscribing to a particular channel, Unsubscribing a channel, Sending
* Notifications to a particular recipient or all subscribers of a Channel etc.
**/
// Essential Imports
// import "hardhat/console.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/docs-v3.x/contracts/utils/Strings.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/docs-v3.x/contracts/math/SafeMath.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/docs-v3.x/contracts/token/ERC20/IERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/docs-v3.x/contracts/proxy/Initializable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/docs-v3.x/contracts/token/ERC20/SafeERC20.sol";
contract EPNSCommV1 is Initializable {
using SafeMath for uint256;
using SafeERC20 for IERC20;
/**
* @notice User Struct that involves imperative details about
* a specific User.
**/
struct User {
// @notice Depicts whether or not a user is ACTIVE
bool userActivated;
// @notice Will be false until public key is emitted
bool publicKeyRegistered;
// @notice Events should not be polled before this block as user doesn't exist
uint256 userStartBlock;
// @notice Keep track of subscribers
uint256 subscribedCount;
/**
* Depicts if User subscribed to a Specific Channel Address
* 1 -> User is Subscribed
* 0 -> User is NOT SUBSCRIBED
**/
mapping(address => uint8) isSubscribed;
// Keeps track of all subscribed channels
mapping(address => uint256) subscribed;
mapping(uint256 => address) mapAddressSubscribed;
}
/** MAPPINGS **/
mapping(address => User) public users;
mapping(address => uint256) public nonces;
mapping(uint256 => address) public mapAddressUsers;
mapping(address => mapping(address => string)) public userToChannelNotifs;
mapping(address => mapping(address => bool)) public delegatedNotificationSenders;
/** STATE VARIABLES **/
address public governance;
address public pushChannelAdmin;
uint256 public chainID;
uint256 public usersCount;
bool public isMigrationComplete;
address public EPNSCoreAddress;
string public chainName;
string public constant name = "EPNS COMM V1";
bytes32 public constant NAME_HASH = keccak256(bytes(name));
bytes32 public constant DOMAIN_TYPEHASH =
keccak256(
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
);
bytes32 public constant SUBSCRIBE_TYPEHASH =
keccak256("Subscribe(address channel,uint256 nonce,uint256 expiry)");
bytes32 public constant UNSUBSCRIBE_TYPEHASH =
keccak256("Unsubscribe(address channel,uint256 nonce,uint256 expiry)");
bytes32 public constant SEND_NOTIFICATION_TYPEHASH =
keccak256(
"SendNotification(address channel,address delegate,address recipient,bytes identity,uint256 nonce,uint256 expiry)"
);
/** EVENTS **/
event SendNotification(
address indexed channel,
address indexed recipient,
bytes identity
);
event UserNotifcationSettingsAdded(
address _channel,
address _user,
uint256 _notifID,
string _notifSettings
);
event AddDelegate(address channel, address delegate);
event RemoveDelegate(address channel, address delegate);
event Subscribe(address indexed channel, address indexed user);
event Unsubscribe(address indexed channel, address indexed user);
event PublicKeyRegistered(address indexed owner, bytes publickey);
event ChannelAlias(string _chainName, uint256 indexed _chainID, address indexed _channelOwnerAddress, string _ethereumChannelAddress);
/** MODIFIERS **/
modifier onlyPushChannelAdmin() {
require(msg.sender == pushChannelAdmin, "EPNSCommV1::onlyPushChannelAdmin: user not pushChannelAdmin");
_;
}
modifier onlyEPNSCore() {
require(msg.sender == EPNSCoreAddress, "EPNSCommV1::onlyEPNSCore: Caller NOT EPNSCore");
_;
}
modifier sendNotifViaSignReq(
address _channel,
address _recipient,
address signatory
) {
require(
(_channel == signatory) ||
(delegatedNotificationSenders[_channel][signatory]) ||
(_recipient == signatory),
"EPNSCommV1::sendNotifViaSignReq: Invalid Channel, Delegate Or Subscriber"
);
_;
}
/* ***************
INITIALIZER
*************** */
function initialize(address _pushChannelAdmin, string memory _chainName) public initializer returns (bool) {
pushChannelAdmin = _pushChannelAdmin;
governance = _pushChannelAdmin;
chainName = _chainName;
chainID = getChainId();
return true;
}
/****************
=> SETTER FUNCTIONS <=
****************/
function verifyChannelAlias(string memory _channelAddress) external{
emit ChannelAlias(chainName, chainID, msg.sender, _channelAddress);
}
function completeMigration() external onlyPushChannelAdmin{
isMigrationComplete = true;
}
function setEPNSCoreAddress(address _coreAddress) external onlyPushChannelAdmin {
EPNSCoreAddress = _coreAddress;
}
function setGovernanceAddress(address _governanceAddress) external onlyPushChannelAdmin{
governance = _governanceAddress;
}
function transferPushChannelAdminControl(address _newAdmin) public onlyPushChannelAdmin {
require(_newAdmin != address(0), "EPNSCommV1::transferPushChannelAdminControl: Invalid Address");
require(_newAdmin != pushChannelAdmin, "EPNSCommV1::transferPushChannelAdminControl: Admin address is same");
pushChannelAdmin = _newAdmin;
}
/****************
=> SUBSCRIBE FUNCTIOANLTIES <=
****************/
/**
* @notice Helper function to check if User is Subscribed to a Specific Address
* @param _channel address of the channel that the user is subscribing to
* @param _user address of the Subscriber
* @return isSubscriber True if User is actually a subscriber of a Channel
**/
function isUserSubscribed(address _channel, address _user)
public
view
returns (bool isSubscriber)
{
User storage user = users[_user];
if (user.isSubscribed[_channel] == 1) {
isSubscriber = true;
}
}
/**
* @notice External Subscribe Function that allows users to Diretly interact with the Base Subscribe function
* @dev Subscribes the caller of the function to a particular Channel
* Takes into Consideration the "msg.sender"
* @param _channel address of the channel that the user is subscribing to
**/
function subscribe(address _channel) external returns (bool) {
_subscribe(_channel, msg.sender);
return true;
}
/**
* @notice Allows users to subscribe a List of Channels at once
*
* @param _channelList array of addresses of the channels that the user wishes to Subscribe
**/
function batchSubscribe(address[] calldata _channelList)
external
returns (bool)
{
for (uint256 i = 0; i < _channelList.length; i++) {
_subscribe(_channelList[i], msg.sender);
}
return true;
}
/**
* @notice This Function helps in migrating the already existing Subscriber's data to the New protocol
*
* @dev Can only be called by pushChannelAdmin
* Can only be called if the Migration is not yet complete, i.e., "isMigrationComplete" boolean must be false
* Subscribes the Users to the respective Channels as per the arguments passed to the function
*
* @param _startIndex starting Index for the LOOP
* @param _endIndex Last Index for the LOOP
* @param _channelList array of addresses of the channels
* @param _usersList array of addresses of the Users or Subscribers of the Channels
**/
function migrateSubscribeData(
uint256 _startIndex,
uint256 _endIndex,
address[] calldata _channelList,
address[] calldata _usersList
) external onlyPushChannelAdmin returns (bool) {
require(
!isMigrationComplete,
"EPNSCommV1::migrateSubscribeData: Migration of Subscribe Data is Complete Already"
);
require(
_channelList.length == _usersList.length,
"EPNSCommV1::migrateSubscribeData: Unequal Arrays passed as Argument"
);
for (uint256 i = _startIndex; i < _endIndex; i++) {
if(isUserSubscribed(_channelList[i], _usersList[i])){
continue;
}else{
_subscribe(_channelList[i], _usersList[i]);
}
}
return true;
}
/**
* @notice Base Subscribe Function that allows users to Subscribe to a Particular Channel
*
* @dev Initializes the User Struct with crucial details about the Channel Subscription
* Addes the caller as a an Activated User of the protocol. (Only if the user hasn't been added already)
*
* @param _channel address of the channel that the user is subscribing to
* @param _user address of the Subscriber
**/
function _subscribe(address _channel, address _user) private {
require(
!isUserSubscribed(_channel, _user),
"EPNSCommV1::_subscribe: User already Subscribed"
);
_addUser(_user);
User storage user = users[_user];
user.isSubscribed[_channel] = 1;
// treat the count as index and update user struct
user.subscribed[_channel] = user.subscribedCount;
user.mapAddressSubscribed[user.subscribedCount] = _channel;
user.subscribedCount = user.subscribedCount.add(1); // Finally increment the subscribed count
// Emit it
emit Subscribe(_channel, _user);
}
/**
* @notice Subscribe Function through Meta TX
* @dev Takes into Consideration the Sign of the User
**/
function subscribeBySig(
address channel,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public {
bytes32 domainSeparator = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
NAME_HASH,
getChainId(),
address(this)
)
);
bytes32 structHash = keccak256(
abi.encode(SUBSCRIBE_TYPEHASH, channel, nonce, expiry)
);
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
);
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "EPNSCommV1::subscribeBySig: Invalid signature");
require(nonce == nonces[signatory]++, "EPNSCommV1::subscribeBySig: Invalid nonce");
require(now <= expiry, "EPNSCommV1::subscribeBySig: Signature expired");
_subscribe(channel, signatory);
}
/**
* @notice Allows EPNSCore contract to call the Base Subscribe function whenever a User Creates his/her own Channel.
* This ensures that the Channel Owner is subscribed to imperative EPNS Channels as well as his/her own Channel.
*
* @dev Only Callable by the EPNSCore. This is to ensure that Users should only able to Subscribe for their own addresses.
* The caller of the main Subscribe function should Either Be the USERS themselves(for their own addresses) or the EPNSCore contract
*
* @param _channel address of the channel that the user is subscribing to
* @param _user address of the Subscriber of a Channel
**/
function subscribeViaCore(address _channel, address _user)
external
onlyEPNSCore
returns (bool)
{
_subscribe(_channel, _user);
return true;
}
/****************
=> USUBSCRIBE FUNCTIOANLTIES <=
****************/
/**
* @notice External Unsubcribe Function that allows users to directly unsubscribe from a particular channel
*
* @dev UnSubscribes the caller of the function from the particular Channel.
* Takes into Consideration the "msg.sender"
*
* @param _channel address of the channel that the user is subscribing to
**/
function unsubscribe(address _channel) external returns (bool){
// Call actual unsubscribe
_unsubscribe(_channel, msg.sender);
return true;
}
/**
* @notice Allows users to unsubscribe from a List of Channels at once
*
* @param _channelList array of addresses of the channels that the user wishes to Unsubscribe
**/
function batchUnsubscribe(address[] calldata _channelList)
external
returns (bool)
{
for (uint256 i = 0; i < _channelList.length; i++) {
_unsubscribe(_channelList[i], msg.sender);
}
return true;
}
/**
* @notice Base Usubscribe Function that allows users to UNSUBSCRIBE from a Particular Channel
* @dev Modifies the User Struct with crucial details about the Channel Unsubscription
* @param _channel address of the channel that the user is subscribing to
* @param _user address of the Subscriber
**/
function _unsubscribe(address _channel, address _user) private {
require(
isUserSubscribed(_channel, _user),
"EPNSCommV1::_unsubscribe: User not subscribed to channel"
);
// Add the channel to gray list so that it can't subscriber the user again as delegated
User storage user = users[_user];
user.isSubscribed[_channel] = 0;
// Remove the mappings and cleanup
// a bit tricky, swap and delete to maintain mapping
// Remove From Users mapping
// Find the id of the channel and swap it with the last id, use channel.memberCount as index
// Slack too deep fix
// address usrSubToSwapAdrr = user.mapAddressSubscribed[user.subscribedCount];
// uint usrSubSwapID = user.subscribed[_channel];
// // swap to last one and then
// user.subscribed[usrSubToSwapAdrr] = usrSubSwapID;
// user.mapAddressSubscribed[usrSubSwapID] = usrSubToSwapAdrr;
user.subscribed[user.mapAddressSubscribed[user.subscribedCount]] = user
.subscribed[_channel];
user.mapAddressSubscribed[user.subscribed[_channel]] = user
.mapAddressSubscribed[user.subscribedCount];
// delete the last one and substract
delete (user.subscribed[_channel]);
delete (user.mapAddressSubscribed[user.subscribedCount]);
user.subscribedCount = user.subscribedCount.sub(1);
// Emit it
emit Unsubscribe(_channel, _user);
}
/**
* @notice Unsubscribe Function through Meta TX
* @dev Takes into Consideration the Signer of the transactioner
**/
function unsubscribeBySig(
address channel,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public {
bytes32 domainSeparator = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
NAME_HASH,
getChainId(),
address(this)
)
);
bytes32 structHash = keccak256(
abi.encode(UNSUBSCRIBE_TYPEHASH, channel, nonce, expiry)
);
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
);
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "EPNSCommV1::unsubscribeBySig: Invalid signature");
require(nonce == nonces[signatory]++, "EPNSCommV1::unsubscribeBySig: Invalid nonce");
require(now <= expiry, "EPNSCommV1::unsubscribeBySig: Signature expired");
_unsubscribe(channel, signatory);
}
/* **************
=> PUBLIC KEY BROADCASTING & USER ADDING FUNCTIONALITIES <=
*************** */
/**
* @notice Activates/Adds a particular User's Address in the Protocol.
* Keeps track of the Total User Count
* @dev Executes its main actions only if the User is not activated yet.
* Does nothing if an address has already been added.
*
* @param _user address of the user
* @return userAlreadyAdded returns whether or not a user is already added.
**/
function _addUser(address _user) private returns (bool userAlreadyAdded) {
if (users[_user].userActivated) {
userAlreadyAdded = true;
} else {
// Activates the user
users[_user].userStartBlock = block.number;
users[_user].userActivated = true;
mapAddressUsers[usersCount] = _user;
usersCount = usersCount.add(1);
}
}
/* @dev Internal system to handle broadcasting of public key,
* A entry point for subscribe, or create channel but is optional
*/
function _broadcastPublicKey(address _userAddr, bytes memory _publicKey)
private
{
// Add the user, will do nothing if added already, but is needed before broadcast
_addUser(_userAddr);
// get address from public key
address userAddr = getWalletFromPublicKey(_publicKey);
if (_userAddr == userAddr) {
// Only change it when verification suceeds, else assume the channel just wants to send group message
users[userAddr].publicKeyRegistered = true;
// Emit the event out
emit PublicKeyRegistered(userAddr, _publicKey);
} else {
revert("Public Key Validation Failed");
}
}
/// @dev Don't forget to add 0x into it
function getWalletFromPublicKey(bytes memory _publicKey)
public
pure
returns (address wallet)
{
if (_publicKey.length == 64) {
wallet = address(uint160(uint256(keccak256(_publicKey))));
} else {
wallet = 0x0000000000000000000000000000000000000000;
}
}
/// @dev Performs action by the user themself to broadcast their public key
function broadcastUserPublicKey(bytes calldata _publicKey) external {
// Will save gas
if (users[msg.sender].publicKeyRegistered) {
// Nothing to do, user already registered
return;
}
// broadcast it
_broadcastPublicKey(msg.sender, _publicKey);
}
/* **************
=> SEND NOTIFICATION FUNCTIONALITIES <=
*************** */
/**
* @notice Allows a Channel Owner to ADD a Delegate for sending Notifications
* Delegate shall be able to send Notification on the Channel's Behalf
* @dev This function will be only be callable by the Channel Owner from the EPNSCore contract.
* NOTE: Verification of whether or not a Channel Address is actually the owner of the Channel, will be done via the PUSH NODES.
*
* @param _delegate address of the delegate who is allowed to Send Notifications
**/
function addDelegate(address _delegate) external {
delegatedNotificationSenders[msg.sender][_delegate] = true;
emit AddDelegate(msg.sender, _delegate);
}
/**
* @notice Allows a Channel Owner to Remove a Delegate's Permission to Send Notification
* @dev This function will be only be callable by the Channel Owner from the EPNSCore contract.
* NOTE: Verification of whether or not a Channel Address is actually the owner of the Channel, will be done via the PUSH NODES.
* @param _delegate address of the delegate who is allowed to Send Notifications
**/
function removeDelegate(address _delegate) external {
delegatedNotificationSenders[msg.sender][_delegate] = false;
emit RemoveDelegate(msg.sender, _delegate);
}
/***
THREE main CALLERS for this function-
1. Channel Owner sends Notif to all Subscribers / Subset of Subscribers / Individual Subscriber
2. Delegatee of Channel sends Notif to Recipients
3. User sends Notifs to Themselvs via a Channel
NOTE: A user can only send notification to their own address
<---------------------------------------------------------------------------------------------->
* When a CHANNEL OWNER Calls the Function and sends a Notif:
* -> We ensure -> "Channel Owner Must be Valid" && "Channel Owner is the Caller"
* -> NOTE - Validation of wether or not an address is a CHANNEL, is done via PUSH NODES
*
* When a Delegatee wants to send Notif to Recipient:
* -> We ensure "Delegate is the Caller" && "Delegatee is Approved by Chnnel Owner"
*
* When User wants to Send a Notif to themselves:
* -> We ensure "Caller of the Function is the Recipient of the Notification"
**/
function _checkNotifReq
(
address _channel,
address _recipient
) private view
{
require(
(_channel == 0x0000000000000000000000000000000000000000 &&
msg.sender == pushChannelAdmin) ||
(_channel == msg.sender) ||
(delegatedNotificationSenders[_channel][msg.sender]) ||
(_recipient == msg.sender),
"EPNSCommV1::_checkNotifReq: Invalid Channel, Delegate or Subscriber"
);
}
/**
* @notice Allows a Channel Owners, Delegates as well as Users to send Notifications
* @dev Emits out notification details once all the requirements are passed.
* @param _channel address of the Channel
* @param _recipient address of the reciever of the Notification
* @param _identity Info about the Notification
**/
function sendNotification(
address _channel,
address _recipient,
bytes memory _identity
) public {
_checkNotifReq(_channel, _recipient);
// Emit the message out
emit SendNotification(_channel, _recipient, _identity);
}
/**
* @notice Base Notification Function that Allows a Channel Owners, Delegates as well as Users to send Notifications
*
* @dev Specifically designed to be called via the EIP 712 send notif function.
* Takes into consideration the Signatory address to perform all the imperative checks
*
* @param _channel address of the Channel
* @param _recipient address of the reciever of the Notification
* @param _signatory address of the SIGNER of the Send Notif Function call transaction
* @param _identity Info about the Notification
**/
function _sendNotification(
address _channel,
address _recipient,
address _signatory,
bytes calldata _identity
)
private
sendNotifViaSignReq(
_channel,
_recipient,
_signatory
)
{
// Emit the message out
emit SendNotification(_channel, _recipient, _identity);
}
/**
* @notice Meta transaction function for Sending Notifications
* @dev Allows the Caller to Simply Sign the transaction to initiate the Send Notif Function
**/
function sendNotifBySig(
address _channel,
address _recipient,
bytes calldata _identity,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 domainSeparator = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
NAME_HASH,
getChainId(),
address(this)
)
);
bytes32 structHash = keccak256(
abi.encode(
SEND_NOTIFICATION_TYPEHASH,
_channel,
_recipient,
_identity,
nonce,
expiry
)
);
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
);
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "EPNSCommV1::sendNotifBySig: Invalid signature");
require(nonce == nonces[signatory]++, "EPNSCommV1::sendNotifBySig: Invalid nonce");
require(now <= expiry, "EPNSCommV1::sendNotifBySig: Signature expired");
_sendNotification(
_channel,
_recipient,
signatory,
_identity
);
}
/* **************
=> User Notification Settings Function <=
*************** */
/**
* @notice Allows Users to Create and Subscribe to a Specific Notication Setting for a Channel.
* @dev Updates the userToChannelNotifs mapping to keep track of a User's Notification Settings for a Specific Channel
*
* Deliminated Notification Settings string contains -> Decimal Representation Notif Settings + Notification Settings
* For instance, for a Notif Setting that looks like -> 3+1-0+2-0+3-1+4-98
* 3 -> Decimal Representation of the Notification Options selected by the User
*
* For Boolean Type Notif Options
* 1-0 -> 1 stands for Option 1 - 0 Means the user didn't choose that Notif Option.
* 3-1 stands for Option 3 - 1 Means the User Selected the 3rd boolean Option
*
* For SLIDER TYPE Notif Options
* 2-0 -> 2 stands for Option 2 - 0 is user's Choice
* 4-98-> 4 stands for Option 4 - 98is user's Choice
*
* @param _channel - Address of the Channel for which the user is creating the Notif settings
* @param _notifID- Decimal Representation of the Options selected by the user
* @param _notifSettings - Deliminated string that depicts the User's Notifcation Settings
*
**/
function changeUserChannelSettings(
address _channel,
uint256 _notifID,
string calldata _notifSettings
) external {
require(
isUserSubscribed(_channel, msg.sender),
"EPNSCommV1::changeUserChannelSettings: User not Subscribed to Channel"
);
string memory notifSetting = string(
abi.encodePacked(Strings.toString(_notifID), "+", _notifSettings)
);
userToChannelNotifs[msg.sender][_channel] = notifSetting;
emit UserNotifcationSettingsAdded(
_channel,
msg.sender,
_notifID,
notifSetting
);
}
function getChainId() internal pure returns (uint256) {
uint256 chainId;
assembly {
chainId := chainid()
}
return chainId;
}
}
```