/// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import { Owned } from "lib/solmate/src/auth/Owned.sol";
import { IERC20 } from "src/interfaces/IERC20.sol";
import { Points } from "src/Points.sol";
import { IBestFriend } from "src/interfaces/IBestFriend.sol";
// BestFriend is a friend you can trust. He is a friend who gives you points for your
// friendship, and he is a friend who will never let you down.
//
// Note the owner is capable of manipulating of determining distribution of points.
//
// Have fun reading it. Hopefully it's bug-free. God bless.
contract BestFriend is Owned(msg.sender), IBestFriend {
// Info of each user.
struct UserInfo {
uint256 lastDepositBlock; // The last block the user deposited/withdrew LP tokens
uint256 amount; // How many LP tokens the user has provided.
uint256 rewardDebt; // Reward debt. See explanation below.
//
// We do some fancy math here. Basically, any point in time, the amount of pointss
// entitled to a user but is pending to be distributed is:
//
// pending reward = (user.amount * pool.accpointsPerShare) - user.rewardDebt
//
// Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens:
// 1. The pool's `accpointsPerShare` (and `lastRewardBlock`) gets updated.
// 2. User receives the pending reward sent to his/her address.
// 3. User's `amount` gets updated.
// 4. User's `rewardDebt` gets updated.
}
// Info of each pool.
struct PoolInfo {
IERC20 lpToken; // Address of LP token contract.
uint256 lastRewardBlock; // Last block number that pointss distribution occurs.
uint256 bonusPointsAccrued; // Total number of bonus points that have been received.
uint256 bonusPointsDistributed; // Bonus points that have been distributed so far.
uint256 accpointsPerShare; // Accumulated points per share, times wag. See below.
}
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
/// @notice POINTS!!
Points public immutable points;
/// @notice Block number when the reward period ends, unless extended
uint256 public endBlock;
/// @notice The number of points distributed per block
uint256 public immutable pointsPerBlock;
/// @notice Information stored relating to the BunnySwap Pool
PoolInfo public pool;
/// @notice Mapping to keep track of stakers
mapping(address => UserInfo) public userInfo;
/// @notice Scale factor for fixed point math, with a meme makerdao name
uint256 public constant wag = 1e12;
/// @notice The address of RabbitRouter
address public immutable router;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event EmergencyWithdraw(address indexed user, uint256 amount);
/*
* @param Points the points contract for onchain points
* @param _pool Starting data for the pool, note if this is misconfigured the contract *will* malfunction
* @param _pointsPerBlock The number of points to distribute per block
* @param _endBlock The block number at which the reward period ends
* @param _router The address of the router contract
*/
constructor(Points _points, PoolInfo memory _pool, uint256 _pointsPerBlock, uint256 _endBlock, address _router) {
points = _points;
pointsPerBlock = _pointsPerBlock;
endBlock = _endBlock;
pool = _pool;
router = _router;
}
/*
* @param newEndBlock The new end block for the new reward period
* @safety Points must be sent to BestFriend in order to fund the extended operation
*/
function extendEndBlock(uint256 newEndBlock) external onlyOwner {
endBlock = newEndBlock;
}
/*//////////////////////////////////////////////////////////////
VIEW
//////////////////////////////////////////////////////////////*/
/*
* @notice View function to see pending points on frontend.
* @param _user Address of user.
*
* @return Pending points reward for a given user.
*/
function pendingpoints(address _user) external view returns (uint256) {
UserInfo storage user = userInfo[_user];
uint256 accpointsPerShare = pool.accpointsPerShare;
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
uint256 lastEligibleBlock = block.number < endBlock ? block.number : endBlock;
if (block.number > pool.lastRewardBlock && lpSupply != 0) {
uint256 multiplier;
if (lastEligibleBlock <= pool.lastRewardBlock) {
multiplier = 0;
} else {
multiplier = lastEligibleBlock - pool.lastRewardBlock;
}
uint256 pointsReward = multiplier * pointsPerBlock + pool.bonusPointsAccrued - pool.bonusPointsDistributed;
accpointsPerShare = accpointsPerShare + pointsReward * wag / lpSupply;
}
return user.amount * accpointsPerShare / wag - user.rewardDebt;
}
/*
* @notice Update reward variables of the given pool to be up-to-date.
*/
function updatePool() public {
if (block.number <= pool.lastRewardBlock) {
return;
}
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
if (lpSupply == 0) {
pool.lastRewardBlock = block.number;
return;
}
uint256 lastEligibleBlock = block.number < endBlock ? block.number : endBlock;
uint256 multiplier;
if (lastEligibleBlock <= pool.lastRewardBlock) {
multiplier = 0;
} else {
multiplier = lastEligibleBlock - pool.lastRewardBlock;
}
uint256 pointsReward = multiplier * pointsPerBlock + pool.bonusPointsAccrued - pool.bonusPointsDistributed;
pool.accpointsPerShare = pool.accpointsPerShare + pointsReward * wag / lpSupply;
pool.lastRewardBlock = block.number;
pool.bonusPointsDistributed = pool.bonusPointsAccrued;
}
/*
* @notice Let your best friend hold onto your LP tokens for points
* @param _amount The amount of LP tokens to deposit
* @param onBehalfOf The address to deposit the LP tokens for, should be msg.sender unless you're the router
*/
function deposit(uint256 _amount, address onBehalfOf) external {
if (onBehalfOf != msg.sender) {
require(router == msg.sender, "deposit: not authorized");
}
UserInfo storage user = userInfo[onBehalfOf];
updatePool();
if (user.amount > 0) {
uint256 pending = ((user.amount * pool.accpointsPerShare) / wag) - user.rewardDebt;
points.transfer(onBehalfOf, pending);
}
pool.lpToken.transferFrom(address(msg.sender), address(this), _amount);
user.amount = user.amount + _amount;
user.rewardDebt = user.amount * pool.accpointsPerShare / wag;
user.lastDepositBlock = block.number;
emit Deposit(onBehalfOf, _amount);
}
/*
* @notice Withdraw LP tokens from BestFriend
* @param _amount The amount of LP tokens to withdraw
* @param onBehalfOf The address to withdraw the LP tokens for, should be msg.sender unless you're the router
*/
function withdraw(uint256 _amount, address onBehalfOf) external {
if (onBehalfOf != msg.sender) {
require(router == msg.sender, "withdraw: not authorized");
}
UserInfo storage user = userInfo[onBehalfOf];
require(user.amount >= _amount, "withdraw: not good");
require(user.lastDepositBlock + 5 < block.number, "withdraw: wait");
updatePool();
uint256 pending = ((user.amount * pool.accpointsPerShare) / wag) - user.rewardDebt;
points.transfer(onBehalfOf, pending);
user.amount = user.amount - _amount;
user.rewardDebt = (user.amount * pool.accpointsPerShare) / wag;
pool.lpToken.transfer(msg.sender, _amount);
emit Withdraw(onBehalfOf, _amount);
}
/*
* @notice Lets a contract (Clubs) add to the rewards pool as a bonus
* @param _amount The amount of points to add to the bonus pool
*/
function addBonusPoints(uint256 _amount) external {
pool.bonusPointsAccrued = pool.bonusPointsAccrued + _amount;
points.transferFrom(msg.sender, address(this), _amount);
}
/*
* @notice Withdraw without collecting rewards, for emergency situations
* @note I don't think any MasterChef fork has ever needed to use this, but kept it
* as it doesn't hurt.
*/
function emergencyWithdraw() external {
UserInfo storage user = userInfo[msg.sender];
pool.lpToken.transfer(address(msg.sender), user.amount);
emit EmergencyWithdraw(msg.sender, user.amount);
user.amount = 0;
user.rewardDebt = 0;
}
}
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.23;
interface IBestFriend {
function addBonusPoints(uint256 _amount) external;
}
/// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
interface IERC20 {
event Approval(address indexed owner, address indexed spender, uint256 value);
event Transfer(address indexed from, address indexed to, uint256 value);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
/// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
interface IPoints {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/// @notice By maintaining ERC20 compatibility, transfers and approvals are easier to index
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
/// @notice Keep track of whether or not a user has claimed their points
event ClaimedA(address indexed _owner, address indexed recipient, uint256 indexed amount);
event ClaimedB(address indexed _owner, address indexed recipient, uint256 indexed amount);
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
/*
* @param proof The merkle proof supplied by the user to prove ownership of the claim
* @param recipient The address that will receive the claimed points
* @param amount The amount of points claimed
*/
function claimA(bytes32[] calldata proof, address recipient, uint256 amount, address[] calldata giftRecipients, uint256 giftAmount) external;
function claimB(bytes32[] calldata proof, address recipient, uint256 amount, address[] calldata giftRecipients, uint256 giftAmount) external;
/*
* @notice Unlike ERC20, Points do not return a boolean on transfer
* @param to The address that will receive the points
* @param value The amount of points to transfer
*/
function transfer(address to, uint256 value) external;
/*
* @notice Unlike ERC20, Points do not return a boolean on transfer
* @param from The address that will send the points
* @param to The address that will receive the points
* @param value The amount of points to transfer
*/
function transferFrom(address from, address to, uint256 value) external;
/*
* @param spender The address that will be allowed to spend the points
* @param amount The amount of points the spender will be allowed to spend
*/
function approve(address spender, uint256 amount) external;
/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/
/*
* @param _owner The address that will be allowed to spend the points
* @param _spender The address that will be allowed to spend the points
*
* @return The amount of points the spender is allowed to spend
*/
function allowance(address _owner, address _spender) external view returns (uint256);
/*
* @param _owner The address that will be checked for points
*
* @return The amount of points the owner has
*/
function balanceOf(address _owner) external view returns (uint256);
/*
* @return The root of the Merkle Tree which stores claims
*/
function merkleRootA() external view returns (bytes32);
/*//////////////////////////////////////////////////////////////
ACCESS CONTROL
//////////////////////////////////////////////////////////////*/
/*
* @param _address The address in question
*
* @return Whether the contract is allowed to interact with points
*/
function isAllowed(address _address) external view returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Gas optimized verification of proof of inclusion for a leaf in a Merkle tree.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/MerkleProofLib.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/MerkleProofLib.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/MerkleProof.sol)
library MerkleProofLib {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MERKLE PROOF VERIFICATION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns whether `leaf` exists in the Merkle tree with `root`, given `proof`.
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf)
internal
pure
returns (bool isValid)
{
/// @solidity memory-safe-assembly
assembly {
if mload(proof) {
// Initialize `offset` to the offset of `proof` elements in memory.
let offset := add(proof, 0x20)
// Left shift by 5 is equivalent to multiplying by 0x20.
let end := add(offset, shl(5, mload(proof)))
// Iterate over proof elements to compute root hash.
for {} 1 {} {
// Slot of `leaf` in scratch space.
// If the condition is true: 0x20, otherwise: 0x00.
let scratch := shl(5, gt(leaf, mload(offset)))
// Store elements to hash contiguously in scratch space.
// Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes.
mstore(scratch, leaf)
mstore(xor(scratch, 0x20), mload(offset))
// Reuse `leaf` to store the hash to reduce stack operations.
leaf := keccak256(0x00, 0x40)
offset := add(offset, 0x20)
if iszero(lt(offset, end)) { break }
}
}
isValid := eq(leaf, root)
}
}
/// @dev Returns whether `leaf` exists in the Merkle tree with `root`, given `proof`.
function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf)
internal
pure
returns (bool isValid)
{
/// @solidity memory-safe-assembly
assembly {
if proof.length {
// Left shift by 5 is equivalent to multiplying by 0x20.
let end := add(proof.offset, shl(5, proof.length))
// Initialize `offset` to the offset of `proof` in the calldata.
let offset := proof.offset
// Iterate over proof elements to compute root hash.
for {} 1 {} {
// Slot of `leaf` in scratch space.
// If the condition is true: 0x20, otherwise: 0x00.
let scratch := shl(5, gt(leaf, calldataload(offset)))
// Store elements to hash contiguously in scratch space.
// Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes.
mstore(scratch, leaf)
mstore(xor(scratch, 0x20), calldataload(offset))
// Reuse `leaf` to store the hash to reduce stack operations.
leaf := keccak256(0x00, 0x40)
offset := add(offset, 0x20)
if iszero(lt(offset, end)) { break }
}
}
isValid := eq(leaf, root)
}
}
/// @dev Returns whether all `leaves` exist in the Merkle tree with `root`,
/// given `proof` and `flags`.
///
/// Note:
/// - Breaking the invariant `flags.length == (leaves.length - 1) + proof.length`
/// will always return false.
/// - The sum of the lengths of `proof` and `leaves` must never overflow.
/// - Any non-zero word in the `flags` array is treated as true.
/// - The memory offset of `proof` must be non-zero
/// (i.e. `proof` is not pointing to the scratch space).
function verifyMultiProof(
bytes32[] memory proof,
bytes32 root,
bytes32[] memory leaves,
bool[] memory flags
) internal pure returns (bool isValid) {
// Rebuilds the root by consuming and producing values on a queue.
// The queue starts with the `leaves` array, and goes into a `hashes` array.
// After the process, the last element on the queue is verified
// to be equal to the `root`.
//
// The `flags` array denotes whether the sibling
// should be popped from the queue (`flag == true`), or
// should be popped from the `proof` (`flag == false`).
/// @solidity memory-safe-assembly
assembly {
// Cache the lengths of the arrays.
let leavesLength := mload(leaves)
let proofLength := mload(proof)
let flagsLength := mload(flags)
// Advance the pointers of the arrays to point to the data.
leaves := add(0x20, leaves)
proof := add(0x20, proof)
flags := add(0x20, flags)
// If the number of flags is correct.
for {} eq(add(leavesLength, proofLength), add(flagsLength, 1)) {} {
// For the case where `proof.length + leaves.length == 1`.
if iszero(flagsLength) {
// `isValid = (proof.length == 1 ? proof[0] : leaves[0]) == root`.
isValid := eq(mload(xor(leaves, mul(xor(proof, leaves), proofLength))), root)
break
}
// The required final proof offset if `flagsLength` is not zero, otherwise zero.
let proofEnd := add(proof, shl(5, proofLength))
// We can use the free memory space for the queue.
// We don't need to allocate, since the queue is temporary.
let hashesFront := mload(0x40)
// Copy the leaves into the hashes.
// Sometimes, a little memory expansion costs less than branching.
// Should cost less, even with a high free memory offset of 0x7d00.
leavesLength := shl(5, leavesLength)
for { let i := 0 } iszero(eq(i, leavesLength)) { i := add(i, 0x20) } {
mstore(add(hashesFront, i), mload(add(leaves, i)))
}
// Compute the back of the hashes.
let hashesBack := add(hashesFront, leavesLength)
// This is the end of the memory for the queue.
// We recycle `flagsLength` to save on stack variables (sometimes save gas).
flagsLength := add(hashesBack, shl(5, flagsLength))
for {} 1 {} {
// Pop from `hashes`.
let a := mload(hashesFront)
// Pop from `hashes`.
let b := mload(add(hashesFront, 0x20))
hashesFront := add(hashesFront, 0x40)
// If the flag is false, load the next proof,
// else, pops from the queue.
if iszero(mload(flags)) {
// Loads the next proof.
b := mload(proof)
proof := add(proof, 0x20)
// Unpop from `hashes`.
hashesFront := sub(hashesFront, 0x20)
}
// Advance to the next flag.
flags := add(flags, 0x20)
// Slot of `a` in scratch space.
// If the condition is true: 0x20, otherwise: 0x00.
let scratch := shl(5, gt(a, b))
// Hash the scratch space and push the result onto the queue.
mstore(scratch, a)
mstore(xor(scratch, 0x20), b)
mstore(hashesBack, keccak256(0x00, 0x40))
hashesBack := add(hashesBack, 0x20)
if iszero(lt(hashesBack, flagsLength)) { break }
}
isValid :=
and(
// Checks if the last value in the queue is same as the root.
eq(mload(sub(hashesBack, 0x20)), root),
// And whether all the proofs are used, if required.
eq(proofEnd, proof)
)
break
}
}
}
/// @dev Returns whether all `leaves` exist in the Merkle tree with `root`,
/// given `proof` and `flags`.
///
/// Note:
/// - Breaking the invariant `flags.length == (leaves.length - 1) + proof.length`
/// will always return false.
/// - Any non-zero word in the `flags` array is treated as true.
/// - The calldata offset of `proof` must be non-zero
/// (i.e. `proof` is from a regular Solidity function with a 4-byte selector).
function verifyMultiProofCalldata(
bytes32[] calldata proof,
bytes32 root,
bytes32[] calldata leaves,
bool[] calldata flags
) internal pure returns (bool isValid) {
// Rebuilds the root by consuming and producing values on a queue.
// The queue starts with the `leaves` array, and goes into a `hashes` array.
// After the process, the last element on the queue is verified
// to be equal to the `root`.
//
// The `flags` array denotes whether the sibling
// should be popped from the queue (`flag == true`), or
// should be popped from the `proof` (`flag == false`).
/// @solidity memory-safe-assembly
assembly {
// If the number of flags is correct.
for {} eq(add(leaves.length, proof.length), add(flags.length, 1)) {} {
// For the case where `proof.length + leaves.length == 1`.
if iszero(flags.length) {
// `isValid = (proof.length == 1 ? proof[0] : leaves[0]) == root`.
// forgefmt: disable-next-item
isValid := eq(
calldataload(
xor(leaves.offset, mul(xor(proof.offset, leaves.offset), proof.length))
),
root
)
break
}
// The required final proof offset if `flagsLength` is not zero, otherwise zero.
let proofEnd := add(proof.offset, shl(5, proof.length))
// We can use the free memory space for the queue.
// We don't need to allocate, since the queue is temporary.
let hashesFront := mload(0x40)
// Copy the leaves into the hashes.
// Sometimes, a little memory expansion costs less than branching.
// Should cost less, even with a high free memory offset of 0x7d00.
calldatacopy(hashesFront, leaves.offset, shl(5, leaves.length))
// Compute the back of the hashes.
let hashesBack := add(hashesFront, shl(5, leaves.length))
// This is the end of the memory for the queue.
// We recycle `flagsLength` to save on stack variables (sometimes save gas).
flags.length := add(hashesBack, shl(5, flags.length))
// We don't need to make a copy of `proof.offset` or `flags.offset`,
// as they are pass-by-value (this trick may not always save gas).
for {} 1 {} {
// Pop from `hashes`.
let a := mload(hashesFront)
// Pop from `hashes`.
let b := mload(add(hashesFront, 0x20))
hashesFront := add(hashesFront, 0x40)
// If the flag is false, load the next proof,
// else, pops from the queue.
if iszero(calldataload(flags.offset)) {
// Loads the next proof.
b := calldataload(proof.offset)
proof.offset := add(proof.offset, 0x20)
// Unpop from `hashes`.
hashesFront := sub(hashesFront, 0x20)
}
// Advance to the next flag offset.
flags.offset := add(flags.offset, 0x20)
// Slot of `a` in scratch space.
// If the condition is true: 0x20, otherwise: 0x00.
let scratch := shl(5, gt(a, b))
// Hash the scratch space and push the result onto the queue.
mstore(scratch, a)
mstore(xor(scratch, 0x20), b)
mstore(hashesBack, keccak256(0x00, 0x40))
hashesBack := add(hashesBack, 0x20)
if iszero(lt(hashesBack, flags.length)) { break }
}
isValid :=
and(
// Checks if the last value in the queue is same as the root.
eq(mload(sub(hashesBack, 0x20)), root),
// And whether all the proofs are used, if required.
eq(proofEnd, proof.offset)
)
break
}
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EMPTY CALLDATA HELPERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns an empty calldata bytes32 array.
function emptyProof() internal pure returns (bytes32[] calldata proof) {
/// @solidity memory-safe-assembly
assembly {
proof.length := 0
}
}
/// @dev Returns an empty calldata bytes32 array.
function emptyLeaves() internal pure returns (bytes32[] calldata leaves) {
/// @solidity memory-safe-assembly
assembly {
leaves.length := 0
}
}
/// @dev Returns an empty calldata bool array.
function emptyFlags() internal pure returns (bool[] calldata flags) {
/// @solidity memory-safe-assembly
assembly {
flags.length := 0
}
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Simple single owner authorization mixin.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Owned.sol)
abstract contract Owned {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event OwnershipTransferred(address indexed user, address indexed newOwner);
/*//////////////////////////////////////////////////////////////
OWNERSHIP STORAGE
//////////////////////////////////////////////////////////////*/
address public owner;
modifier onlyOwner() virtual {
require(msg.sender == owner, "UNAUTHORIZED");
_;
}
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(address _owner) {
owner = _owner;
emit OwnershipTransferred(address(0), _owner);
}
/*//////////////////////////////////////////////////////////////
OWNERSHIP LOGIC
//////////////////////////////////////////////////////////////*/
function transferOwnership(address newOwner) public virtual onlyOwner {
owner = newOwner;
emit OwnershipTransferred(msg.sender, newOwner);
}
}
/// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import { MerkleProofLib } from "lib/solady/src/utils/MerkleProofLib.sol";
import { Owned } from "lib/solmate/src/auth/Owned.sol";
import { IPoints } from "./interfaces/IPoints.sol";
/// @title Points
/// @author CopyPaste
/// @notice A Standard Implementation of Points for FriendTechV2
contract Points is Owned(msg.sender), IPoints {
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
/// Merkle Root for the point claims
bytes32 public immutable merkleRootA;
bytes32 public immutable merkleRootB;
mapping(address user => uint256 balance) public balanceOf;
mapping(address owner => mapping(address spender => uint256 amount)) public allowance;
mapping(address user => bool hasClaimedA) public claimedA;
mapping(address user => bool hasClaimedB) public claimedB;
/// The totalSupply of points
uint256 public totalSupply;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/*
* @param _merkleRootA The merkle root for first the point claims
* @param _merkleRootB The merkle root for second the point claims
*/
constructor(bytes32 _merkleRootA, bytes32 _merkleRootB) {
merkleRootA = _merkleRootA;
merkleRootB = _merkleRootB;
}
/*//////////////////////////////////////////////////////////////
METADATA
//////////////////////////////////////////////////////////////*/
string public name = "FRIEND";
string public symbol = "FRIEND";
uint8 public decimals = 18;
/*//////////////////////////////////////////////////////////////
ACCESS CONTROL
//////////////////////////////////////////////////////////////*/
mapping(address _contract => bool isWhitelisted) public isAllowed;
mapping(bytes4 functionSignature => bool canBeCalled) public isOpen;
modifier onlyWhitelisted() {
require(isAllowed[msg.sender] || isOpen[msg.sig], "WHITELIST");
_;
}
/*
* @param _contract The contract address allowed to interact with points
* @param toggle The toggle for whether or not the contract is allowed
*/
function toggleContract(address _contract, bool toggle) external onlyOwner {
isAllowed[_contract] = toggle;
}
/*
* @param function The function to open to the public
* @param toggle The toggle for whether or not the contract is allowed
*/
function toggleFunction(bytes4 functionSig, bool toggle) external onlyOwner {
isOpen[functionSig] = toggle;
}
/*
* @param to address to recieve the points
* @param amount The amount of points to mint
*/
function mint(address to, uint256 amount) external onlyOwner {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
/*//////////////////////////////////////////////////////////////
TRANSFER LOGIC
//////////////////////////////////////////////////////////////*/
/*
* @param proof The merkle proof supplied by the user to prove ownership of the claim
* @param recipient The address that will receive the claimed points
* @param amount The amount of points claimed
*/
function claimA(bytes32[] calldata proof, address recipient, uint256 amount, address[] calldata giftRecipients, uint256 giftAmount) external {
require(!claimedA[msg.sender], "Points: already claimed");
require(giftAmount <= amount, "Points: gift amount exceeds claim"); // Would underflow anyway but you can never be too safe
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, amount))));
require(MerkleProofLib.verifyCalldata(proof, merkleRootA, leaf), "Points: invalid proof");
claimedA[msg.sender] = true;
if (giftAmount != 0) {
uint256 perUser = giftAmount / giftRecipients.length;
amount -= giftAmount;
for (uint256 i = 0; i < giftRecipients.length; ) {
balanceOf[giftRecipients[i]] += perUser;
unchecked {
++i;
}
}
}
totalSupply += amount + giftAmount;
// Mint the points to the recipient
unchecked {
balanceOf[recipient] += amount;
}
emit ClaimedA(msg.sender, recipient, amount);
}
/*
* @param proof The merkle proof supplied by the user to prove ownership of the claim
* @param recipient The address that will receive the claimed points
* @param amount The amount of points claimed
*/
function claimB(bytes32[] calldata proof, address recipient, uint256 amount, address[] calldata giftRecipients, uint256 giftAmount) external {
require(!claimedB[msg.sender], "Points: already claimed");
require(giftAmount <= amount, "Points: gift amount exceeds claim"); // Would underflow anyway but you can never be too safe
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, amount))));
require(MerkleProofLib.verifyCalldata(proof, merkleRootB, leaf), "Points: invalid proof");
claimedB[msg.sender] = true;
if (giftAmount != 0) {
uint256 perUser = giftAmount / giftRecipients.length;
amount -= giftAmount;
for (uint256 i = 0; i < giftRecipients.length; ) {
balanceOf[giftRecipients[i]] += perUser;
unchecked {
++i;
}
}
}
totalSupply += amount + giftAmount;
// Mint the points to the recipient
unchecked {
balanceOf[recipient] += amount;
}
emit ClaimedB(msg.sender, recipient, amount);
}
/*
* @notice Unlike ERC20, Points do not return a boolean on transfer
* @param to The address that will receive the points
* @param amount The amount of points to transfer
*/
function transfer(address to, uint256 amount) external onlyWhitelisted {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
}
/*
* @notice Unlike ERC20, Points do not return a boolean on transfer
* @param from The address that will send the points
* @param to The address that will receive the points
* @param amount The amount of points to transfer
*/
function transferFrom(address from, address to, uint256 amount) external onlyWhitelisted {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
}
/*
* @param spender The address that will be allowed to spend the points
* @param amount The amount of points the spender will be allowed to spend
*/
function approve(address spender, uint256 amount) external onlyWhitelisted {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
}
}
{
"compilationTarget": {
"src/BestFriend.sol": "BestFriend"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"appendCBOR": false,
"bytecodeHash": "none"
},
"optimizer": {
"enabled": true,
"runs": 1000000
},
"remappings": [
":ds-test/=lib/forge-std/lib/ds-test/src/",
":forge-std/=lib/forge-std/src/",
":murky/=lib/murky/",
":openzeppelin-contracts/=lib/murky/lib/openzeppelin-contracts/",
":solady/=lib/solady/src/",
":solmate/=lib/solmate/src/"
]
}
[{"inputs":[{"internalType":"contract Points","name":"_points","type":"address"},{"components":[{"internalType":"contract IERC20","name":"lpToken","type":"address"},{"internalType":"uint256","name":"lastRewardBlock","type":"uint256"},{"internalType":"uint256","name":"bonusPointsAccrued","type":"uint256"},{"internalType":"uint256","name":"bonusPointsDistributed","type":"uint256"},{"internalType":"uint256","name":"accpointsPerShare","type":"uint256"}],"internalType":"struct BestFriend.PoolInfo","name":"_pool","type":"tuple"},{"internalType":"uint256","name":"_pointsPerBlock","type":"uint256"},{"internalType":"uint256","name":"_endBlock","type":"uint256"},{"internalType":"address","name":"_router","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"EmergencyWithdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"addBonusPoints","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"onBehalfOf","type":"address"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"emergencyWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"endBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"newEndBlock","type":"uint256"}],"name":"extendEndBlock","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"}],"name":"pendingpoints","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"points","outputs":[{"internalType":"contract Points","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pointsPerBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pool","outputs":[{"internalType":"contract IERC20","name":"lpToken","type":"address"},{"internalType":"uint256","name":"lastRewardBlock","type":"uint256"},{"internalType":"uint256","name":"bonusPointsAccrued","type":"uint256"},{"internalType":"uint256","name":"bonusPointsDistributed","type":"uint256"},{"internalType":"uint256","name":"accpointsPerShare","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"router","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"updatePool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"userInfo","outputs":[{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"rewardDebt","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"wag","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"onBehalfOf","type":"address"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]