Skip to content

Commit

Permalink
Top up voucher
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyjams committed Jun 4, 2024
1 parent c0f2938 commit 448892e
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## vNEXT
- Top up voucher. (#23)
- Claim task part 2 - Add voucher tests. (#21)
- Claim task part 1 - Solidity with minimal tests. (#20)
- Compute deal price with proper volume. (#19)
Expand Down
2 changes: 2 additions & 0 deletions contracts/IVoucherHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface IVoucherHub {
uint256 voucherType,
uint256 value
);
event VoucherToppedUp(address indexed voucher, uint256 value, uint256 voucherExpiration);
event VoucherDebited(address indexed voucher, uint256 sponsoredAmount);
event VoucherRefunded(address indexed voucher, uint256 amount);
event VoucherTypeCreated(uint256 indexed id, string description, uint256 duration);
Expand All @@ -33,6 +34,7 @@ interface IVoucherHub {
uint256 voucherType,
uint256 value
) external returns (address voucherAddress);
function topUpVoucher(address voucher, uint256 value) external;
function debitVoucher(
uint256 voucherTypeId,
address app,
Expand Down
19 changes: 19 additions & 0 deletions contracts/VoucherHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,25 @@ contract VoucherHub is
emit VoucherCreated(voucherAddress, owner, voucherExpiration, voucherType, value);
}

/**
* Top up a voucher by increasing its balance and extending the validity
* period before expiration.
* @param voucher The address of the voucher.
* @param value The amount of credits to top up.
*/
function topUpVoucher(address voucher, uint256 value) external onlyRole(VOUCHER_MANAGER_ROLE) {
VoucherHubStorage storage $ = _getVoucherHubStorage();
require($._isVoucher[voucher], "VoucherHub: unknown voucher");
if (value > 0) {
_mint(voucher, value); // VCHR
IERC20($._iexecPoco).transfer(voucher, value); // SRLC
}
uint256 voucherExpiration = block.timestamp +
$.voucherTypes[Voucher(voucher).getType()].duration;
Voucher(voucher).setExpiration(voucherExpiration);
emit VoucherToppedUp(voucher, value, voucherExpiration);
}

/**
* Debit voucher balance when used assets are eligible to voucher sponsoring.
* @notice (1) If this function is called by an account which is not a voucher,
Expand Down
2 changes: 2 additions & 0 deletions contracts/beacon/IVoucher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {IexecLibOrders_v5} from "@iexec/poco/contracts/libs/IexecLibOrders_v5.so
pragma solidity ^0.8.20;

interface IVoucher {
event ExpirationUpdated(uint256 expiration);
event AccountAuthorized(address indexed account);
event AccountUnauthorized(address indexed account);
event OrdersMatchedWithVoucher(bytes32 dealId);
Expand All @@ -32,6 +33,7 @@ interface IVoucher {
function getVoucherHub() external view returns (address);
function getType() external view returns (uint256);
function getExpiration() external view returns (uint256);
function setExpiration(uint256 expiration) external;
function getBalance() external view returns (uint256);
function isAccountAuthorized(address account) external view returns (bool);
function getSponsoredAmount(bytes32 dealId) external view returns (uint256);
Expand Down
11 changes: 11 additions & 0 deletions contracts/beacon/Voucher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,17 @@ contract Voucher is Initializable, IVoucher {
return $._expiration;
}

/**
* Set the expiration timestamp of the voucher.
* @param expiration The expiration timestamp.
*/
function setExpiration(uint256 expiration) external {
VoucherStorage storage $ = _getVoucherStorage();
require(msg.sender == $._voucherHub, "Voucher: sender is not VoucherHub");
$._expiration = expiration;
emit ExpirationUpdated(expiration);
}

/**
* Retrieve the type of the voucher.
* @return voucherType The type of the voucher.
Expand Down
89 changes: 87 additions & 2 deletions test/VoucherHub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { expect } from 'chai';
import { AddressLike, BigNumberish } from 'ethers';
import { AddressLike, BigNumberish, Wallet } from 'ethers';
import { ethers } from 'hardhat';
import * as commonUtils from '../scripts/common';
import * as voucherHubUtils from '../scripts/voucherHubUtils';
import * as voucherUtils from '../scripts/voucherUtils';
import { IexecPocoMock, IexecPocoMock__factory, Voucher } from '../typechain-types';
import {
IexecPocoMock,
IexecPocoMock__factory,
Voucher,
Voucher__factory,
} from '../typechain-types';
import { VoucherHub } from '../typechain-types/contracts';
import { random } from './utils/address-utils';

Expand Down Expand Up @@ -659,6 +664,86 @@ describe('VoucherHub', function () {
});
});

describe('Top up voucher', function () {
let [voucherOwner1, anyone]: SignerWithAddress[] = [];
let voucherHub: VoucherHub;
let voucherAddress: string;

beforeEach(async function () {
({ voucherHub, voucherOwner1, anyone } = await loadFixture(deployFixture));
await voucherHubWithAssetEligibilityManagerSigner
.createVoucherType(description, duration)
.then((tx) => tx.wait());
voucherAddress = await voucherHubWithVoucherManagerSigner
.createVoucher(voucherOwner1, voucherType, voucherValue)
.then((tx) => tx.wait())
.then(() => voucherHub.getVoucher(voucherOwner1));
});

it('Should top up voucher', async function () {
const topUpValue = 123n; // arbitrary value
const voucherCreditBalanceBefore = await voucherHub.balanceOf(voucherAddress);
const voucherRlcBalanceBefore = await iexecPocoInstance.balanceOf(voucherAddress);

const tx = await voucherHubWithVoucherManagerSigner.topUpVoucher(
voucherAddress,
topUpValue,
);
const txReceipt = await tx.wait();
const expectedExpiration = await commonUtils.getExpectedExpiration(duration, txReceipt);
await expect(tx)
.to.emit(voucherHub, 'VoucherToppedUp')
.withArgs(voucherAddress, topUpValue, expectedExpiration);
const voucherCreditBalanceAfter = await voucherHub.balanceOf(voucherAddress);
const voucherRlcBalanceAfter = await iexecPocoInstance.balanceOf(voucherAddress);
expect(voucherCreditBalanceAfter)
.equal(voucherCreditBalanceBefore + topUpValue)
.equal(voucherRlcBalanceBefore + topUpValue)
.equal(voucherRlcBalanceAfter);
expect(
await Voucher__factory.connect(voucherAddress, anyone).getExpiration(),
).to.be.equal(expectedExpiration);
});

it('Should top up voucher without value', async function () {
const voucherCreditBalanceBefore = await voucherHub.balanceOf(voucherAddress);
const voucherRlcBalanceBefore = await iexecPocoInstance.balanceOf(voucherAddress);
const expirationBefore = await Voucher__factory.connect(
voucherAddress,
anyone,
).getExpiration();

await expect(
voucherHubWithVoucherManagerSigner.topUpVoucher(voucherAddress, 0n),
).to.emit(voucherHub, 'VoucherToppedUp');
expect(await voucherHub.balanceOf(voucherAddress))
.equal(voucherCreditBalanceBefore)
.equal(await iexecPocoInstance.balanceOf(voucherAddress))
.equal(voucherRlcBalanceBefore);
expect(
// expiration is extended even if top up value is empty
await Voucher__factory.connect(voucherAddress, anyone).getExpiration(),
).greaterThan(expirationBefore);
});

it('Should not top up by anyone', async function () {
await expect(
voucherHub
.connect(anyone)
.topUpVoucher(Wallet.createRandom().address, voucherValue),
).to.revertedWithCustomError(voucherHub, 'AccessControlUnauthorizedAccount');
});

it('Should not top up unknown voucher', async function () {
await expect(
voucherHubWithVoucherManagerSigner.topUpVoucher(
Wallet.createRandom().address,
voucherValue,
),
).to.revertedWith('VoucherHub: unknown voucher');
});
});

describe('Debit voucher', function () {
let [voucherOwner1, voucherOwner2, voucher, anyone]: SignerWithAddress[] = [];
let voucherHub: VoucherHub;
Expand Down
36 changes: 36 additions & 0 deletions test/beacon/Voucher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
UpgradeableBeacon,
Voucher,
VoucherHub,
VoucherProxy__factory,
Voucher__factory,
} from '../../typechain-types';
import { random } from '../utils/address-utils';
Expand Down Expand Up @@ -209,6 +210,41 @@ describe('Voucher', function () {
});
});

describe('Voucher expiration', function () {
it('Should get expiration', async function () {
const expiration = 42; // arbitrary value
const voucher = await new VoucherProxy__factory(anyone)
.deploy(await beacon.getAddress())
.then((tx) => tx.waitForDeployment())
.then((proxy) => proxy.getAddress())
.then((address) => Voucher__factory.connect(address, anyone));
await voucher.initialize(anyone.address, voucherHub, expiration, voucherType);
expect(await voucher.getExpiration()).equal(expiration);
});

it('Should set expiration', async function () {
const voucherHubSigner = await ethers.getImpersonatedSigner(
await voucherHub.getAddress(),
);
const expirationBefore = await voucherAsAnyone.getExpiration();
const expirationAfter = 13; // arbitrary value
await expect(
await voucherAsAnyone.connect(voucherHubSigner).setExpiration(expirationAfter),
)
.to.emit(voucherAsAnyone, 'ExpirationUpdated')
.withArgs(expirationAfter);
expect(await voucherAsAnyone.getExpiration())
.equal(expirationAfter)
.not.equal(expirationBefore);
});

it('Should not set expiration', async function () {
await expect(
voucherAsAnyone.setExpiration(789), // any expiration value is fine
).to.be.revertedWith('Voucher: sender is not VoucherHub');
});
});

describe('Authorization', function () {
it('Should authorize an account', async function () {
expect(await voucherAsOwner.isAccountAuthorized(anyone.address)).to.be.false;
Expand Down

0 comments on commit 448892e

Please sign in to comment.