Skip to content

Commit

Permalink
Merge branch 'develop' into feature/clean-todos
Browse files Browse the repository at this point in the history
  • Loading branch information
gfournieriExec committed Jun 3, 2024
2 parents e65e7ed + d137dfc commit 0f1f6b5
Show file tree
Hide file tree
Showing 14 changed files with 575 additions and 60 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# 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)
- Match orders boost through voucher. (#16)
Expand Down
5 changes: 4 additions & 1 deletion 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 @@ -39,8 +40,10 @@ interface IVoucherHub {
address dataset,
uint256 datasetPrice,
address workerpool,
uint256 workerpoolPrice
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
27 changes: 25 additions & 2 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 @@ -165,6 +173,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 @@ -178,13 +187,17 @@ 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.
* @param dataset The dataset address.
* @param datasetPrice The dataset price.
* @param workerpool The workerpool address.
* @param workerpoolPrice The workerpool price.
* @param volume Volume of the deal.
*/
function debitVoucher(
uint256 voucherTypeId,
Expand All @@ -193,7 +206,8 @@ contract VoucherHub is
address dataset,
uint256 datasetPrice,
address workerpool,
uint256 workerpoolPrice
uint256 workerpoolPrice,
uint256 volume
) external returns (uint256 sponsoredAmount) {
VoucherHubStorage storage $ = _getVoucherHubStorage();
mapping(address asset => bool) storage eligible = $.matchOrdersEligibility[voucherTypeId];
Expand All @@ -206,13 +220,22 @@ contract VoucherHub is
if (eligible[workerpool]) {
sponsoredAmount += workerpoolPrice;
}
sponsoredAmount = Math.min(balanceOf(msg.sender), sponsoredAmount);
sponsoredAmount = Math.min(balanceOf(msg.sender), sponsoredAmount * volume);
if (sponsoredAmount > 0) {
_burn(msg.sender, sponsoredAmount);
emit VoucherDebited(msg.sender, sponsoredAmount);
}
}

/**
* 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);
}

/**
* Get iExec Poco address used by vouchers.
*/
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
135 changes: 130 additions & 5 deletions contracts/beacon/Voucher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@

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 {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 {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.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 @@ -28,6 +35,8 @@ contract Voucher is Initializable, IVoucher {
uint256 _type;
mapping(address => bool) _authorizedAccounts;
mapping(bytes32 dealId => uint256) _sponsoredAmounts;
// Save refunded tasks to disable replay attacks.
mapping(bytes32 taskId => bool) _refundedTasks;
address _owner;
}

Expand Down Expand Up @@ -172,6 +181,71 @@ contract Voucher is Initializable, 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 type of the voucher.
* @return voucherType The type of the voucher.
Expand Down Expand Up @@ -244,12 +318,57 @@ contract Voucher is Initializable, 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,
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;
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 All @@ -275,19 +394,25 @@ contract Voucher is Initializable, IVoucher {
uint256 appPrice = appOrder.appprice;
uint256 datasetPrice = datasetOrder.datasetprice;
uint256 workerpoolPrice = workerpoolOrder.workerpoolprice;

uint256 volume = IexecPocoAccessors(iexecPoco).computeDealVolume(
appOrder,
datasetOrder,
workerpoolOrder,
requestOrder
);
uint256 dealPrice = datasetOrder.dataset != address(0)
? (appPrice + datasetPrice + workerpoolPrice) * volume
: (appPrice + workerpoolPrice) * volume;
sponsoredAmount = voucherHub.debitVoucher(
voucherTypeId,
appOrder.app,
appPrice,
datasetOrder.dataset,
datasetPrice,
workerpoolOrder.workerpool,
workerpoolPrice
workerpoolPrice,
volume
);
// TODO: Compute volume and set dealPrice = taskPrice * volume instead of curent dealPrice
uint256 dealPrice = appPrice + datasetPrice + workerpoolPrice;

if (sponsoredAmount != dealPrice) {
// Transfer non-sponsored amount from the iExec account of the
// requester to the iExec account of the voucher
Expand Down
Loading

0 comments on commit 0f1f6b5

Please sign in to comment.