Skip to content

Commit

Permalink
feat(contract): add points for tipping (#2272)
Browse files Browse the repository at this point in the history
Co-authored-by: g <[email protected]>
  • Loading branch information
shuhuiluo and giuseppecrj authored Feb 7, 2025
1 parent 392f2ad commit e304536
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"address": "0x1265FD7f05E9700DA0A4d459BcE38376bb3a1475"
"address": "0x2E0F3c30e5164dA369B40C531b7EDAa497734807"
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"address": "0xE5e358e11986E48461652ebc2111ddce05AaA0d8"
"address": "0x09e710567B7fB466E3e904a70a8bD60aFcE51f70"
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"address": "0x6b2179482Ee8432a3650A0742e3968c96b1F487f"
"address": "0x32a6c393a7F2434975B29f1Ff7dF838A03610F31"
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"address": "0xc6F202b4A06FAbf5c838289514e41fd37611C9e1"
"address": "0x98323458f8A214c805a0615764e66CA4b3c26A7c"
}
3 changes: 2 additions & 1 deletion contracts/src/airdrop/points/ITownsPoints.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ pragma solidity ^0.8.23;
interface ITownsPointsBase {
enum Action {
JoinSpace,
CheckIn
CheckIn,
Tip
}

/// @notice Emitted when a user successfully checks in and receives points
Expand Down
6 changes: 6 additions & 0 deletions contracts/src/airdrop/points/TownsPoints.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ contract TownsPoints is IERC20Metadata, ITownsPoints, OwnableBase, Facet {
);
(points, ) = CheckIn.getPointsAndStreak(lastCheckIn, streak, currentTime);
}

if (action == Action.Tip) {
uint256 protocolFee = abi.decode(data, (uint256));
// 1 pt per 0.0003 ETH
points = (protocolFee * 10_000) / 3;
}
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
Expand Down
39 changes: 39 additions & 0 deletions contracts/src/spaces/facets/points/PointsProxyLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

// interfaces
import {IImplementationRegistry} from "contracts/src/factory/facets/registry/IImplementationRegistry.sol";
import {ITownsPoints} from "contracts/src/airdrop/points/ITownsPoints.sol";

// libraries

// contracts
import {MembershipStorage} from "contracts/src/spaces/facets/membership/MembershipStorage.sol";

/// @title PointsProxyLib
/// @notice Library for interacting with the TownsPoints contract
library PointsProxyLib {
/// @dev The implementation ID for the TownsPoints contract
bytes32 internal constant POINTS_DIAMOND = bytes32("RiverAirdrop");

// =============================================================
// GETTERS
// =============================================================

function airdropDiamond() internal view returns (address) {
return
IImplementationRegistry(MembershipStorage.layout().spaceFactory)
.getLatestImplementation(POINTS_DIAMOND);
}

function getPoints(
ITownsPoints.Action action,
bytes memory data
) internal view returns (uint256) {
return ITownsPoints(airdropDiamond()).getPoints(action, data);
}

function mint(address to, uint256 amount) internal {
ITownsPoints(airdropDiamond()).mint(to, amount);
}
}
44 changes: 41 additions & 3 deletions contracts/src/spaces/facets/tipping/TippingFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,56 @@ pragma solidity ^0.8.23;

// interfaces
import {ITipping} from "./ITipping.sol";
import {ITownsPointsBase} from "contracts/src/airdrop/points/ITownsPoints.sol";
import {IPlatformRequirements} from "contracts/src/factory/facets/platform/requirements/IPlatformRequirements.sol";

// libraries
import {TippingBase} from "./TippingBase.sol";
import {CustomRevert} from "contracts/src/utils/libraries/CustomRevert.sol";
import {PointsProxyLib} from "contracts/src/spaces/facets/points/PointsProxyLib.sol";
import {MembershipStorage} from "contracts/src/spaces/facets/membership/MembershipStorage.sol";
import {BasisPoints} from "contracts/src/utils/libraries/BasisPoints.sol";
import {CurrencyTransfer} from "contracts/src/utils/libraries/CurrencyTransfer.sol";

// contracts
import {ERC721ABase} from "contracts/src/diamond/facets/token/ERC721A/ERC721ABase.sol";
import {Facet} from "@river-build/diamond/src/facets/Facet.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";

contract TippingFacet is ITipping, ERC721ABase, Facet {
contract TippingFacet is ITipping, ERC721ABase, Facet, ReentrancyGuard {
function __Tipping_init() external onlyInitializing {
_addInterface(type(ITipping).interfaceId);
}

/// @inheritdoc ITipping
function tip(TipRequest calldata tipRequest) external payable {
function tip(TipRequest calldata tipRequest) external payable nonReentrant {
_validateTipRequest(
msg.sender,
tipRequest.receiver,
tipRequest.currency,
tipRequest.amount
);

uint256 tipAmount = tipRequest.amount;

if (tipRequest.currency == CurrencyTransfer.NATIVE_TOKEN) {
uint256 protocolFee = _payProtocol(msg.sender, tipRequest.amount);
tipAmount = tipRequest.amount - protocolFee;

uint256 points = PointsProxyLib.getPoints(
ITownsPointsBase.Action.Tip,
abi.encode(protocolFee)
);

PointsProxyLib.mint(msg.sender, points);
}

TippingBase.tip(
msg.sender,
tipRequest.receiver,
tipRequest.tokenId,
tipRequest.currency,
tipRequest.amount
tipAmount
);

emit Tip(
Expand Down Expand Up @@ -87,4 +108,21 @@ contract TippingFacet is ITipping, ERC721ABase, Facet {
if (sender == receiver) CustomRevert.revertWith(CannotTipSelf.selector);
if (amount == 0) CustomRevert.revertWith(AmountIsZero.selector);
}

function _payProtocol(
address sender,
uint256 amount
) internal returns (uint256 protocolFee) {
MembershipStorage.Layout storage ds = MembershipStorage.layout();
IPlatformRequirements platform = IPlatformRequirements(ds.spaceFactory);

protocolFee = BasisPoints.calculate(amount, 50); // 0.5%

CurrencyTransfer.transferCurrency(
CurrencyTransfer.NATIVE_TOKEN,
sender,
platform.getFeeRecipient(),
protocolFee
);
}
}
63 changes: 31 additions & 32 deletions contracts/test/spaces/tipping/Tipping.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
pragma solidity ^0.8.23;

// interfaces
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ITippingBase} from "contracts/src/spaces/facets/tipping/ITipping.sol";
import {IERC721AQueryable} from "contracts/src/diamond/facets/token/ERC721A/extensions/IERC721AQueryable.sol";
import {IERC721ABase} from "contracts/src/diamond/facets/token/ERC721A/IERC721A.sol";
import {ITownsPoints, ITownsPointsBase} from "contracts/src/airdrop/points/ITownsPoints.sol";
import {IPlatformRequirements} from "contracts/src/factory/facets/platform/requirements/IPlatformRequirements.sol";

// libraries
import {CurrencyTransfer} from "contracts/src/utils/libraries/CurrencyTransfer.sol";
import {BasisPoints} from "contracts/src/utils/libraries/BasisPoints.sol";

// contracts
import {TippingFacet} from "contracts/src/spaces/facets/tipping/TippingFacet.sol";
Expand All @@ -27,14 +31,20 @@ contract TippingTest is BaseSetup, ITippingBase, IERC721ABase {
MembershipFacet internal membership;
IERC721AQueryable internal token;
MockERC20 internal mockERC20;
ITownsPoints internal points;

address internal platformRecipient;

function setUp() public override {
super.setUp();

tipping = TippingFacet(everyoneSpace);
introspection = IntrospectionFacet(everyoneSpace);
membership = MembershipFacet(everyoneSpace);
token = IERC721AQueryable(everyoneSpace);
mockERC20 = MockERC20(deployERC20.deploy(deployer));
points = ITownsPoints(riverAirdrop);
platformRecipient = IPlatformRequirements(spaceFactory).getFeeRecipient();
}

modifier givenUsersAreMembers(address sender, address receiver) {
Expand All @@ -61,11 +71,17 @@ contract TippingTest is BaseSetup, ITippingBase, IERC721ABase {
bytes32 messageId,
bytes32 channelId
) external givenUsersAreMembers(sender, receiver) {
amount = bound(amount, 0.01 ether, 1 ether);
vm.assume(sender != platformRecipient);
vm.assume(receiver != platformRecipient);
amount = bound(amount, 0.0003 ether, 1 ether);

uint256 initialBalance = receiver.balance;
uint256[] memory tokens = token.tokensOfOwner(receiver);
uint256 tokenId = tokens[0];
uint256 initialPointBalance = IERC20(address(points)).balanceOf(sender);
uint256 tokenId = token.tokensOfOwner(receiver)[0];

uint256 protocolFee = BasisPoints.calculate(amount, 50); // 0.5%
uint256 tipAmount = amount - protocolFee;

hoax(sender, amount);
vm.expectEmit(address(tipping));
emit Tip(
Expand All @@ -88,19 +104,24 @@ contract TippingTest is BaseSetup, ITippingBase, IERC721ABase {
channelId: channelId
})
);
uint256 gasUsed = vm.stopSnapshotGas();

assertLt(gasUsed, 200_000);
assertEq(receiver.balance - initialBalance, amount);
assertEq(sender.balance, 0);
assertLt(vm.stopSnapshotGas(), 400_000);
assertEq(receiver.balance - initialBalance, tipAmount, "receiver balance");
assertEq(platformRecipient.balance, protocolFee, "protocol fee");
assertEq(sender.balance, 0, "sender balance");
assertEq(
IERC20(address(points)).balanceOf(sender) - initialPointBalance,
(protocolFee * 10_000) / 3,
"points minted"
);
assertEq(
tipping.tipsByCurrencyAndTokenId(tokenId, CurrencyTransfer.NATIVE_TOKEN),
amount
tipAmount
);
assertEq(tipping.totalTipsByCurrency(CurrencyTransfer.NATIVE_TOKEN), 1);
assertEq(
tipping.tipAmountByCurrency(CurrencyTransfer.NATIVE_TOKEN),
amount
tipAmount
);
assertContains(tipping.tippingCurrencies(), CurrencyTransfer.NATIVE_TOKEN);
}
Expand Down Expand Up @@ -145,7 +166,7 @@ contract TippingTest is BaseSetup, ITippingBase, IERC721ABase {
uint256 gasUsed = vm.stopSnapshotGas();
vm.stopPrank();

assertLt(gasUsed, 200_000);
assertLt(gasUsed, 300_000);
assertEq(mockERC20.balanceOf(sender), 0);
assertEq(mockERC20.balanceOf(receiver), amount);
assertEq(
Expand All @@ -157,28 +178,6 @@ contract TippingTest is BaseSetup, ITippingBase, IERC721ABase {
assertContains(tipping.tippingCurrencies(), address(mockERC20));
}

// function test_revertWhenTokenDoesNotExist(
// uint256 tokenId,
// uint256 amount,
// address receiver,
// bytes32 messageId,
// bytes32 channelId
// ) external {
// vm.assume(tokenId != 0); // tokenId cannot be 0 because that would be the founder token id

// vm.expectRevert(OwnerQueryForNonexistentToken.selector);
// tipping.tip(
// TipRequest({
// receiver: receiver,
// tokenId: tokenId,
// currency: CurrencyTransfer.NATIVE_TOKEN,
// amount: amount,
// messageId: messageId,
// channelId: channelId
// })
// );
// }

function test_revertWhenCurrencyIsZero(
address sender,
address receiver,
Expand Down

0 comments on commit e304536

Please sign in to comment.