pragma solidity ^0.5.0;
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract Context {
// Empty internal constructor, to prevent people from mistakenly deploying
// an instance of this contract, which should be used via inheritance.
constructor () internal { }
// solhint-disable-previous-line no-empty-blocks
function _msgSender() internal view returns (address payable) {
return msg.sender;
}
function _msgData() internal view returns (bytes memory) {
this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
return msg.data;
}
}
contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor () internal {
address msgSender = _msgSender();
_owner = msgSender;
emit OwnershipTransferred(address(0), msgSender);
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner(), "Ownable: caller is not the owner");
_;
}
/**
* @dev Returns true if the caller is the current owner.
*/
function isOwner() public view returns (bool) {
return _msgSender() == _owner;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0), "Ownable: new owner is the zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
contract MerkleAirdrop is Ownable {
struct Airdrop {
bytes32 root;
string dataURI;
bool paused;
mapping(address => bool) awarded;
}
/// Events
event Start(uint id);
event PauseChange(uint id, bool paused);
event Award(uint id, address recipient, uint amount);
/// State
mapping(uint => Airdrop) public airdrops;
IERC20 public token;
address public approver;
uint public airdropsCount;
// Errors
string private constant ERROR_AWARDED = "AWARDED";
string private constant ERROR_INVALID = "INVALID";
string private constant ERROR_PAUSED = "PAUSED";
function setToken(address _token, address _approver) public onlyOwner {
token = IERC20(_token);
approver = _approver;
}
/**
* @notice Start a new airdrop `_root` / `_dataURI`
* @param _root New airdrop merkle root
* @param _dataURI Data URI for airdrop data
*/
function start(bytes32 _root, string memory _dataURI) public onlyOwner {
uint id = ++airdropsCount; // start at 1
airdrops[id] = Airdrop(_root, _dataURI, false);
emit Start(id);
}
/**
* @notice Pause or resume an airdrop `_id` / `_paused`
* @param _id The airdrop to change status
* @param _paused Pause to resume
*/
function setPause(uint _id, bool _paused) public onlyOwner {
require(_id <= airdropsCount, ERROR_INVALID);
airdrops[_id].paused = _paused;
emit PauseChange(_id, _paused);
}
/**
* @notice Award from airdrop
* @param _id Airdrop id
* @param _recipient Airdrop recipient
* @param _amount The token amount
* @param _proof Merkle proof to correspond to data supplied
*/
function award(uint _id, address _recipient, uint256 _amount, bytes32[] memory _proof) public {
require( _id <= airdropsCount, ERROR_INVALID );
Airdrop storage airdrop = airdrops[_id];
require( !airdrop.paused, ERROR_PAUSED );
bytes32 hash = keccak256(abi.encodePacked(_recipient, _amount));
require( validate(airdrop.root, _proof, hash), ERROR_INVALID );
require( !airdrops[_id].awarded[_recipient], ERROR_AWARDED );
airdrops[_id].awarded[_recipient] = true;
token.transferFrom(approver, _recipient, _amount);
emit Award(_id, _recipient, _amount);
}
/**
* @notice Award from airdrop
* @param _ids Airdrop ids
* @param _recipient Recepient of award
* @param _amounts The amounts
* @param _proofs Merkle proofs
* @param _proofLengths Merkle proof lengths
*/
function awardFromMany(uint[] memory _ids, address _recipient, uint[] memory _amounts, bytes memory _proofs, uint[] memory _proofLengths) public {
uint totalAmount;
uint marker = 32;
for (uint i = 0; i < _ids.length; i++) {
uint id = _ids[i];
require( id <= airdropsCount, ERROR_INVALID );
require( !airdrops[id].paused, ERROR_PAUSED );
bytes32[] memory proof = extractProof(_proofs, marker, _proofLengths[i]);
marker += _proofLengths[i]*32;
bytes32 hash = keccak256(abi.encodePacked(_recipient, _amounts[i]));
require( validate(airdrops[id].root, proof, hash), ERROR_INVALID );
require( !airdrops[id].awarded[_recipient], ERROR_AWARDED );
airdrops[id].awarded[_recipient] = true;
totalAmount += _amounts[i];
emit Award(id, _recipient, _amounts[i]);
}
token.transferFrom(approver, _recipient, totalAmount);
}
function extractProof(bytes memory _proofs, uint _marker, uint proofLength) public pure returns (bytes32[] memory proof) {
proof = new bytes32[](proofLength);
bytes32 el;
for (uint j = 0; j < proofLength; j++) {
assembly {
el := mload(add(_proofs, _marker))
}
proof[j] = el;
_marker += 32;
}
}
function validate(bytes32 root, bytes32[] memory proof, bytes32 hash) public pure returns (bool) {
for (uint i = 0; i < proof.length; i++) {
if (hash < proof[i]) {
hash = keccak256(abi.encodePacked(hash, proof[i]));
} else {
hash = keccak256(abi.encodePacked(proof[i], hash));
}
}
return hash == root;
}
/**
* @notice Check if recipient:`_recipient` awarded from airdrop:`_id`
* @param _id Airdrop id
* @param _recipient Recipient to check
*/
function awarded(uint _id, address _recipient) public view returns(bool) {
return airdrops[_id].awarded[_recipient];
}
}
{
"compilationTarget": {
"MerkleAirdrop.sol": "MerkleAirdrop"
},
"evmVersion": "istanbul",
"libraries": {},
"optimizer": {
"enabled": true,
"runs": 1
},
"remappings": []
}
[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Award","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bool","name":"paused","type":"bool"}],"name":"PauseChange","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Start","type":"event"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"airdrops","outputs":[{"internalType":"bytes32","name":"root","type":"bytes32"},{"internalType":"string","name":"dataURI","type":"string"},{"internalType":"bool","name":"paused","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"airdropsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"approver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"_id","type":"uint256"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"bytes32[]","name":"_proof","type":"bytes32[]"}],"name":"award","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256[]","name":"_ids","type":"uint256[]"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"bytes","name":"_proofs","type":"bytes"},{"internalType":"uint256[]","name":"_proofLengths","type":"uint256[]"}],"name":"awardFromMany","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"_id","type":"uint256"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"awarded","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes","name":"_proofs","type":"bytes"},{"internalType":"uint256","name":"_marker","type":"uint256"},{"internalType":"uint256","name":"proofLength","type":"uint256"}],"name":"extractProof","outputs":[{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"_id","type":"uint256"},{"internalType":"bool","name":"_paused","type":"bool"}],"name":"setPause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_approver","type":"address"}],"name":"setToken","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"_root","type":"bytes32"},{"internalType":"string","name":"_dataURI","type":"string"}],"name":"start","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"token","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"},{"internalType":"bytes32","name":"hash","type":"bytes32"}],"name":"validate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"}]