Skip to content

Commit

Permalink
Merge pull request #6 from RSSNext/feat/addPurchase
Browse files Browse the repository at this point in the history
feat: add purchase method
  • Loading branch information
iavl authored Oct 21, 2024
2 parents d287b71 + ddd9bd4 commit 9bb740f
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 39 deletions.
71 changes: 65 additions & 6 deletions deployments/PowerToken.abi
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,34 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "purchase",
"inputs": [
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "feedId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "taxBasisPoints",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "removeUser",
Expand Down Expand Up @@ -754,6 +782,37 @@
],
"anonymous": false
},
{
"type": "event",
"name": "Purchase",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "feedId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "RoleAdminChanged",
Expand Down Expand Up @@ -997,6 +1056,11 @@
}
]
},
{
"type": "error",
"name": "AmountIsZero",
"inputs": []
},
{
"type": "error",
"name": "ERC20InsufficientAllowance",
Expand Down Expand Up @@ -1131,12 +1195,7 @@
},
{
"type": "error",
"name": "TipAmountIsZero",
"inputs": []
},
{
"type": "error",
"name": "TipReceiverIsEmpty",
"name": "ReceiverIsEmpty",
"inputs": []
}
]
70 changes: 47 additions & 23 deletions src/PowerToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -130,34 +130,21 @@ contract PowerToken is
}

/// @inheritdoc IPowerToken
function tip(uint256 amount, address to, bytes32 feedId, uint256 taxBasisPoints)
function purchase(uint256 amount, address to, bytes32 feedId, uint256 taxBasisPoints)
external
override
{
if (amount == 0) revert TipAmountIsZero();
if (feedId == bytes32(0) && to == address(0)) revert TipReceiverIsEmpty();
if (balanceOf(msg.sender) < amount) revert InsufficientBalanceAndPoints();

if (_pointsBalancesV2[msg.sender] >= amount) {
_pointsBalancesV2[msg.sender] -= amount;
} else {
_pointsBalancesV2[msg.sender] = 0;
}
uint256 tax = _getTaxAmount(taxBasisPoints, amount);

uint256 tipAmount = amount - tax;

address receiver = to != address(0) ? to : address(this);
if (receiver == address(this)) {
_feedBalances[feedId] += tipAmount;
}
uint256 purchaseAmount = _payWithTax(msg.sender, to, feedId, amount, taxBasisPoints);

if (tax > 0) {
_transfer(msg.sender, ADMIN, tax);
emit TaxCollected(ADMIN, tax);
}
emit Purchase(msg.sender, to, feedId, purchaseAmount);
}

_transfer(msg.sender, receiver, tipAmount);
/// @inheritdoc IPowerToken
function tip(uint256 amount, address to, bytes32 feedId, uint256 taxBasisPoints)
external
override
{
uint256 tipAmount = _payWithTax(msg.sender, to, feedId, amount, taxBasisPoints);

emit Tip(msg.sender, to, feedId, tipAmount);
}
Expand Down Expand Up @@ -231,6 +218,43 @@ contract PowerToken is
return super.transferFrom(from, to, value);
}

function _payWithTax(
address from,
address to,
bytes32 feedId,
uint256 amount,
uint256 taxBasisPoints
) internal returns (uint256) {
if (amount == 0) revert AmountIsZero();

if (balanceOf(from) < amount) revert InsufficientBalanceAndPoints();

if (feedId == bytes32(0) && to == address(0)) revert ReceiverIsEmpty();

if (_pointsBalancesV2[from] >= amount) {
_pointsBalancesV2[from] -= amount;
} else {
_pointsBalancesV2[from] = 0;
}
uint256 tax = _getTaxAmount(taxBasisPoints, amount);

if (tax > 0) {
_transfer(msg.sender, ADMIN, tax);
emit TaxCollected(ADMIN, tax);
}

uint256 tipAmount = amount - tax;

address receiver = to != address(0) ? to : address(this);
if (receiver == address(this)) {
_feedBalances[feedId] += tipAmount;
}

_transfer(msg.sender, receiver, tipAmount);

return tipAmount;
}

/**
* @dev Issues points to a specified address by transferring tokens from the token contract.
*/
Expand Down
8 changes: 4 additions & 4 deletions src/interfaces/IErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
pragma solidity 0.8.22;

interface IErrors {
/// @dev Tip parameter is empty.
error TipReceiverIsEmpty();
/// @dev receiver is empty.
error ReceiverIsEmpty();

/// @dev Points receiver is invalid.
error PointsInvalidReceiver(bytes32);

/// @dev Tip amount is zero.
error TipAmountIsZero();
/// @dev Amount is zero.
error AmountIsZero();

/// @dev Insufficient balance and points.
error InsufficientBalanceAndPoints();
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/IEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ interface IEvents {
* @dev Emitted when points are tipped from one address to another.
*/
event Tip(address indexed from, address indexed to, bytes32 indexed feedId, uint256 amount);
/**
* @dev Emitted when points are paid from one address to another for some purchase.
*/
event Purchase(
address indexed from, address indexed to, bytes32 indexed feedId, uint256 amount
);

/**
* @dev Emitted when points are airdropped to an address.
*/
Expand Down
14 changes: 13 additions & 1 deletion src/interfaces/IPowerToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,21 @@ interface IPowerToken {
*/
function airdrop(address to, uint256 amount, uint256 taxBasisPoints) external;

/**
* @notice Purchases with token points. If token points are not enough, it will try the balance.
* @param amount The amount of token points to send.
* @param to The address to send the token points. It can be empty.
* @param feedId The feed id. It can be empty.
* @param taxBasisPoints The tax basis points.
* @dev The to and feedId are optional, but at least one of them must be provided.
* If both are provided, the `to` will be used.
*/
function purchase(uint256 amount, address to, bytes32 feedId, uint256 taxBasisPoints)
external;

/**
* @notice Tips with token points. If token points are not enough, it will try the balance.
* @param amount The amount of token points to send. It can be empty.
* @param amount The amount of token points to send.
* @param to The address to send the token points. It can be empty.
* @param feedId The feed id. It can be empty.
* @param taxBasisPoints The tax basis points.
Expand Down
27 changes: 22 additions & 5 deletions test/PowerToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -479,17 +479,20 @@ contract PowerTokenTest is Utils, IErrors, IEvents, ERC20Upgradeable {
}

function testTipFail() public {
// case 1: TipAmountIsZero
vm.expectRevert(abi.encodeWithSelector(TipAmountIsZero.selector));
_mintPoints(alice, 100);
vm.startPrank(alice);
// case 1: AmountIsZero
vm.expectRevert(abi.encodeWithSelector(AmountIsZero.selector));
_token.tip(0, bob, "", 0);

// case 2: TipReceiverIsEmpty
vm.expectRevert(abi.encodeWithSelector(TipReceiverIsEmpty.selector));
// case 2: ReceiverIsEmpty
vm.expectRevert(abi.encodeWithSelector(ReceiverIsEmpty.selector));
_token.tip(1, address(0x0), "", 0);

// case 3: InsufficientBalanceAndPoints
vm.expectRevert(abi.encodeWithSelector(InsufficientBalanceAndPoints.selector));
_token.tip(1, bob, "", 0);
_token.tip(101, bob, "", 0);
vm.stopPrank();

// case 4: InsufficientBalanceToTransfer
_mintPoints(charlie, 100);
Expand All @@ -503,6 +506,20 @@ contract PowerTokenTest is Utils, IErrors, IEvents, ERC20Upgradeable {
_token.tip(200, david, "", 0);
}

function testPurchase() public {
_mintPoints(alice, 100 ether);

vm.prank(alice);
expectEmit();
emit Purchase(alice, address(0), "0x1234", 10 ether);
_token.purchase(10 ether, address(0), "0x1234", 0);

vm.prank(alice);
expectEmit();
emit Purchase(alice, address(0), "0x1234", 9 ether);
_token.purchase(10 ether, address(0), "0x1234", 1000);
}

function testWithdrawByFeedId(uint256 amount) public {
amount = bound(amount, 1, 100 ether);
uint256 initialPoints = 10 * amount;
Expand Down

0 comments on commit 9bb740f

Please sign in to comment.