Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Claim task part 1 - Solidity with minimal tests #20

Merged
merged 42 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5e1ac48
Add some todos
zguesmi May 20, 2024
04189df
Use updated version of poco
zguesmi May 21, 2024
bb92f4e
Init task claim function
zguesmi May 22, 2024
900a4f8
Merge branch 'feature/voucher-test-refactoring' into feature/task-claim
zguesmi May 22, 2024
819cf15
Merge branch 'feature/voucher-test-refactoring' into feature/task-claim
zguesmi May 22, 2024
54a561b
Merge branch 'feature/voucher-test-refactoring' into feature/task-claim
zguesmi May 22, 2024
cef0997
Add first claim test
zguesmi May 22, 2024
c8dc54c
Clean tests
zguesmi May 23, 2024
a262e58
Merge branch 'feature/voucher-test-refactoring' into feature/task-claim
zguesmi May 23, 2024
c586647
Add tests of happy path for classic and boost modes
zguesmi May 23, 2024
167c3bf
Add todo test
zguesmi May 23, 2024
3109a68
Add more claim tests
zguesmi May 24, 2024
831ac28
Add claim tests
zguesmi May 24, 2024
d4969a4
Update claim solidity function
zguesmi May 24, 2024
0ce53dc
Merge branch 'develop' into feature/task-claim
zguesmi May 24, 2024
3565eba
Clean
zguesmi May 24, 2024
d889141
Rename state variable
zguesmi May 24, 2024
3ab51aa
Add last claim tests
zguesmi May 24, 2024
68080f8
Clean
zguesmi May 24, 2024
af10412
Update changelog
zguesmi May 24, 2024
43581c8
Move claimed tasks storage to voucherHub
zguesmi May 24, 2024
801524a
Add voucherHub refund tests
zguesmi May 26, 2024
6ebd8a7
Resize PR
zguesmi May 27, 2024
201eb48
Resize PR
zguesmi May 27, 2024
fd5533d
Decrease sponsored amount after refund
zguesmi May 27, 2024
2fbf332
Read deal sponsor from poco and save only sponsored amount
zguesmi May 27, 2024
2266fc3
Rename state variable
zguesmi May 28, 2024
4d64738
Fix upgrade tests
zguesmi May 28, 2024
f8d9f47
Separate claim and claimBoost functions
zguesmi May 28, 2024
468b144
Clean tests
zguesmi May 28, 2024
8a04710
Refactor claim code
zguesmi May 28, 2024
0f1b5f3
Use develop branch of poco
zguesmi May 28, 2024
e584a1b
Remove test todo
zguesmi May 28, 2024
2d62dec
Reordre function
zguesmi May 28, 2024
1fdd243
Pass taskPrice instead of dealPrice to refund function
zguesmi May 29, 2024
8c9e07c
Remove TODO
zguesmi May 29, 2024
7a156ed
Dont update deal sponsored amount in storage
zguesmi May 29, 2024
07639f1
Update test
zguesmi May 29, 2024
cd2e63f
Clean
zguesmi May 29, 2024
a23bd17
Merge branch 'develop' into feature/task-claim
zguesmi May 29, 2024
bf8ec14
Fix tests
zguesmi May 29, 2024
938e7d4
Disable Yul details config to fix compilation
zguesmi May 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## vNEXT
- Claim task part 1 - Solidity with minimal tests. (#20)
- Compute deal price with proper volume. (#19)
- Refactor voucher tests file. (#18)
- Use real poco address if available at deployment. (#17)
Expand Down
2 changes: 2 additions & 0 deletions contracts/IVoucherHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface IVoucherHub {
uint256 value
);
event VoucherDebited(address indexed voucher, uint256 sponsoredAmount);
event VoucherRefunded(address indexed voucher, uint256 amount);
event VoucherTypeCreated(uint256 indexed id, string description, uint256 duration);
event VoucherTypeDescriptionUpdated(uint256 indexed id, string description);
event VoucherTypeDurationUpdated(uint256 indexed id, uint256 duration);
Expand All @@ -42,6 +43,7 @@ interface IVoucherHub {
uint256 workerpoolPrice,
uint256 volume
) external returns (uint256 sponsoredAmount);
function refundVoucher(uint256 amount) external;

function getIexecPoco() external view returns (address);
function getVoucherBeacon() external view returns (address);
Expand Down
21 changes: 21 additions & 0 deletions contracts/VoucherHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ contract VoucherHub is
bytes32 _voucherCreationCodeHash;
VoucherType[] voucherTypes;
mapping(uint256 voucherTypeId => mapping(address asset => bool)) matchOrdersEligibility;
// Track created vouchers to avoid replay in certain operations such as refund.
mapping(address voucherAddress => bool) _isVoucher;
}

modifier whenVoucherTypeExists(uint256 id) {
Expand All @@ -55,6 +57,12 @@ contract VoucherHub is
_;
}

modifier onlyVoucher() {
VoucherHubStorage storage $ = _getVoucherHubStorage();
require($._isVoucher[msg.sender], "VoucherHub: sender is not voucher");
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand Down Expand Up @@ -166,6 +174,7 @@ contract VoucherHub is
Voucher(voucherAddress).initialize(owner, address(this), voucherExpiration, voucherType);
IERC20($._iexecPoco).transfer(voucherAddress, value); // SRLC
_mint(voucherAddress, value); // VCHR
$._isVoucher[voucherAddress] = true;
emit VoucherCreated(voucherAddress, owner, voucherExpiration, voucherType, value);
}

Expand All @@ -179,6 +188,9 @@ contract VoucherHub is
* possible to try to debit the voucher in best effort mode (In short: "use
* voucher if possible"), before trying other payment methods.
*
* Note: no need for "onlyVoucher" modifier because if the sender is not a voucher,
* its balance would be null, then "_burn()" would revert.
*
* @param voucherTypeId The type ID of the voucher to debit.
* @param app The app address.
* @param appPrice The app price.
Expand Down Expand Up @@ -216,6 +228,15 @@ contract VoucherHub is
}
}

/**
* Refund sender if it is a voucher.
* @param amount value to be refunded
*/
function refundVoucher(uint256 amount) external onlyVoucher {
_mint(msg.sender, amount);
emit VoucherRefunded(msg.sender, amount);
}

// TODO make view functions external whenever possible.

/**
Expand Down
3 changes: 3 additions & 0 deletions contracts/beacon/IVoucher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface IVoucher {
event AccountUnauthorized(address indexed account);
event OrdersMatchedWithVoucher(bytes32 dealId);
event OrdersBoostMatchedWithVoucher(bytes32 dealId);
event TaskClaimedWithVoucher(bytes32 taskId);

function authorizeAccount(address account) external;
function unauthorizeAccount(address account) external;
Expand All @@ -25,6 +26,8 @@ interface IVoucher {
IexecLibOrders_v5.WorkerpoolOrder calldata workerpoolOrder,
IexecLibOrders_v5.RequestOrder calldata requestOrder
) external returns (bytes32);
function claim(bytes32 taskId) external;
function claimBoost(bytes32 dealId, uint256 taskIndex) external;

function getVoucherHub() external view returns (address);
function getType() external view returns (uint256);
Expand Down
119 changes: 118 additions & 1 deletion contracts/beacon/Voucher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@

pragma solidity ^0.8.20;

import {IexecLibCore_v5} from "@iexec/poco/contracts/libs/IexecLibCore_v5.sol";
import {IexecLibOrders_v5} from "@iexec/poco/contracts/libs/IexecLibOrders_v5.sol";
import {IexecPoco1} from "@iexec/poco/contracts/modules/interfaces/IexecPoco1.v8.sol";
import {IexecPocoBoost} from "@iexec/poco/contracts/modules/interfaces/IexecPocoBoost.sol";
import {IexecPoco2} from "@iexec/poco/contracts/modules/interfaces/IexecPoco2.v8.sol";
import {IexecPocoAccessors} from "@iexec/poco/contracts/modules/interfaces/IexecPocoAccessors.sol";
import {IexecPocoBoost} from "@iexec/poco/contracts/modules/interfaces/IexecPocoBoost.sol";
import {IexecPocoBoostAccessors} from "@iexec/poco/contracts/modules/interfaces/IexecPocoBoostAccessors.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IVoucherHub} from "../IVoucherHub.sol";
import {IVoucher} from "./IVoucher.sol";

// TODO disable transferOwnership()

/**
* @title Implementation of the voucher contract.
* Deployed along the Beacon contract using "Upgrades" plugin of OZ.
Expand All @@ -29,6 +34,8 @@ contract Voucher is OwnableUpgradeable, IVoucher {
uint256 _type;
mapping(address => bool) _authorizedAccounts;
mapping(bytes32 dealId => uint256) _sponsoredAmounts;
// Save refunded tasks to disable replay attacks.
mapping(bytes32 taskId => bool) _refundedTasks;
}

modifier onlyAuthorized() {
Expand Down Expand Up @@ -170,6 +177,71 @@ contract Voucher is OwnableUpgradeable, IVoucher {
return dealId;
}

/**
* Claim failed task on PoCo then refund voucher and requester.
* @param taskId id of the task
*/
function claim(bytes32 taskId) external {
VoucherStorage storage $ = _getVoucherStorage();
IVoucherHub voucherHub = IVoucherHub($._voucherHub);
address iexecPoco = voucherHub.getIexecPoco();
IexecLibCore_v5.Task memory task = IexecPocoAccessors(iexecPoco).viewTask(taskId);
// Claim task on PoCo if not already claimed.
// This implicitly validates that the task and its deal exist.
if (task.status != IexecLibCore_v5.TaskStatusEnum.FAILED) {
IexecPoco2(iexecPoco).claim(taskId);
}
IexecLibCore_v5.Deal memory deal = IexecPocoAccessors(iexecPoco).viewDeal(task.dealid);
// If the deal was matched by the voucher, then the voucher should be refunded.
// If the deal was partially or not sponsored by the voucher, then the requester
// should be refunded.
if (deal.sponsor == address(this)) {
_refundVoucherAndRequester(
voucherHub,
iexecPoco,
taskId,
deal.app.price + deal.dataset.price + deal.workerpool.price, // taskPrice
task.dealid,
deal.botSize,
deal.requester
);
}
emit TaskClaimedWithVoucher(taskId);
}

/**
* Claim failed Boost task on PoCo then refund voucher and requester.
* @param dealId id of the task's deal
* @param taskIndex task's index in the deal
*/
function claimBoost(bytes32 dealId, uint256 taskIndex) external {
VoucherStorage storage $ = _getVoucherStorage();
IVoucherHub voucherHub = IVoucherHub($._voucherHub);
address iexecPoco = voucherHub.getIexecPoco();
bytes32 taskId = keccak256(abi.encodePacked(dealId, taskIndex));
IexecLibCore_v5.Task memory task = IexecPocoAccessors(iexecPoco).viewTask(taskId);
// Claim task on PoCo if not already claimed.
// This implicitly validates that the task and its deal exist.
if (task.status != IexecLibCore_v5.TaskStatusEnum.FAILED) {
IexecPocoBoost(iexecPoco).claimBoost(dealId, taskIndex);
}
IexecLibCore_v5.DealBoost memory deal = IexecPocoBoostAccessors(iexecPoco).viewDealBoost(
dealId
);
if (deal.sponsor == address(this)) {
_refundVoucherAndRequester(
voucherHub,
iexecPoco,
taskId,
deal.appPrice + deal.datasetPrice + deal.workerpoolPrice, // taskPrice
dealId,
deal.botSize,
deal.requester
);
}
emit TaskClaimedWithVoucher(taskId);
}

/**
* Retrieve the address of the voucher hub associated with the voucher.
* @return voucherHubAddress The address of the voucher hub.
Expand Down Expand Up @@ -234,12 +306,57 @@ contract Voucher is OwnableUpgradeable, IVoucher {
$._authorizedAccounts[account] = isAuthorized;
}

/**
* Ask VoucherHub to refund voucher for a failed task and
* send non-sponsored part back to the requester when needed.
* @param voucherHub hub
* @param iexecPoco address of PoCo contract
* @param taskId id of the task
* @param taskPrice price paid per task at match orders
* @param dealId task's deal id
* @param dealVolume number of tasks in the deal
* @param requester of the task
*/
function _refundVoucherAndRequester(
IVoucherHub voucherHub,
zguesmi marked this conversation as resolved.
Show resolved Hide resolved
address iexecPoco,
bytes32 taskId,
uint256 taskPrice,
bytes32 dealId,
uint256 dealVolume,
address requester
) private {
VoucherStorage storage $ = _getVoucherStorage();
require(!$._refundedTasks[taskId], "Voucher: task already refunded");
$._refundedTasks[taskId] = true;
if (taskPrice != 0) {
uint256 dealSponsoredAmount = $._sponsoredAmounts[dealId];
// A positive remainder is possible when the voucher balance is less than
// the sponsorable amount. Min(balance, dealSponsoredAmount) is computed
// at match orders.
// TODO !! do something with the remainder.
uint256 taskSponsoredAmount = dealSponsoredAmount / dealVolume;
jeremyjams marked this conversation as resolved.
Show resolved Hide resolved
if (taskSponsoredAmount != 0) {
// If the voucher did fully/partially sponsor the deal then mint voucher
// credits back.
voucherHub.refundVoucher(taskSponsoredAmount);
}
if (taskSponsoredAmount < taskPrice) {
// If the deal was not sponsored or partially sponsored
// by the voucher then send the non-sponsored part back
// to the requester.
IERC20(iexecPoco).transfer(requester, taskPrice - taskSponsoredAmount);
}
}
}

function _getVoucherStorage() private pure returns (VoucherStorage storage $) {
assembly {
$.slot := VOUCHER_STORAGE_LOCATION
}
}

// TODO move this function before private view functions.
/**
* @dev Debit voucher and transfer non-sponsored amount from requester's account.
*
Expand Down
Loading