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

LOAS: add revoke function #45

Merged
merged 2 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"compiler-version": ["error", "^0.8.0"],
"func-param-name-mixedcase": "error",
"modifier-name-mixedcase": "error",
"private-vars-leading-underscore": "error"
"private-vars-leading-underscore": "error",
"avoid-low-level-calls": "off"
}
}
62 changes: 60 additions & 2 deletions artifacts/contracts/token/LOAS.sol/LOAS.json

Large diffs are not rendered by default.

51 changes: 48 additions & 3 deletions contracts/token/LOAS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ error InvalidClaimPeriod();
// OAS is zero.
error NoAmount();

// Something not found.
error NotFound();

// Invalid minter address.
error InvalidMinter();

// Invalid revoker address.
error InvalidRevoker();

// Over claimable OAS.
error OverAmount();

Expand Down Expand Up @@ -46,6 +52,7 @@ contract LOAS is ERC20 {
uint64 since;
uint64 until;
address from;
uint256 revoked;
}

/**********************
Expand All @@ -62,6 +69,7 @@ contract LOAS is ERC20 {
event Mint(address indexed to, uint256 amount, uint256 since, uint256 until);
event Claim(address indexed holder, uint256 amount);
event Renounce(address indexed holder, uint256 amount);
event Revoke(address indexed original, address indexed holder, uint256 amount);
event Allow(address indexed original, address indexed transferable);

/***************
Expand Down Expand Up @@ -91,7 +99,7 @@ contract LOAS is ERC20 {
if (msg.value == 0) revert NoAmount();

_mint(to, msg.value);
claimInfo[to] = ClaimInfo(msg.value, 0, since, until, msg.sender);
claimInfo[to] = ClaimInfo(msg.value, 0, since, until, msg.sender, 0);
originalClaimer[to] = to;

emit Mint(to, msg.value, since, until);
Expand Down Expand Up @@ -149,7 +157,8 @@ contract LOAS is ERC20 {
if (amount == 0) revert NoAmount();

ClaimInfo storage originalClaimInfo = claimInfo[originalClaimer[msg.sender]];
if (amount > originalClaimInfo.amount - originalClaimInfo.claimed) revert OverAmount();
uint256 remainingAmount = originalClaimInfo.amount - originalClaimInfo.claimed - originalClaimInfo.revoked;
if (amount > remainingAmount) revert OverAmount();

_burn(msg.sender, amount);
(bool success, ) = originalClaimInfo.from.call{ value: amount }("");
Expand All @@ -158,6 +167,42 @@ contract LOAS is ERC20 {
emit Renounce(originalClaimer[msg.sender], amount);
}

/**
* Revoke the LOAS from the holder.
* As the default behavior, only the locked amount can be revoked.
* otherwise, the amount can be specified.
* @param holder Address of the holder.
* @param amount_ Amount of the LOAS.
*/
function revoke(address holder, uint256 amount_) external {
address original = originalClaimer[holder];
if (original == address(0)) revert NotFound();
// Only the minter can revoke the LOAS.
ClaimInfo storage originalClaimInfo = claimInfo[original];
if (originalClaimInfo.from != msg.sender) revert InvalidRevoker();

// Determine the amount to revoke.
uint256 amount = amount_;
if (amount == 0) {
// As a default, revoke only the locked amount.
uint256 remainingAmount = originalClaimInfo.amount - originalClaimInfo.claimed - originalClaimInfo.revoked;
uint256 currentClaimableOAS = getClaimableOAS(original) - originalClaimInfo.claimed;
if (remainingAmount <= currentClaimableOAS) revert NoAmount(); // Sanity check
amount = remainingAmount - currentClaimableOAS;
}

// Check over amount.
if (balanceOf(holder) < amount) revert OverAmount();

// Revoke the LOAS.
originalClaimInfo.revoked += amount;
_burn(holder, amount);
(bool success, ) = msg.sender.call{ value: amount }("");
if (!success) revert TransferFailed();

emit Revoke(original, holder, amount);
}

/**
* Bulk transfer
* @param tos List of receipient address.
Expand Down Expand Up @@ -223,7 +268,7 @@ contract LOAS is ERC20 {
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
uint256 /*amount*/
) internal view override {
if (from == address(0) || to == address(0)) return;
if (originalClaimer[from] == originalClaimer[to]) return;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oasys-genesis-contract",
"version": "1.6.2",
"version": "1.7.0",
"description": "The genesis contracts of Oasys Blockchain",
"main": "index.js",
"scripts": {
Expand Down
173 changes: 173 additions & 0 deletions test/token/LOAS.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { ethers, network } from 'hardhat'
import { Contract } from 'ethers'
import { SignerWithAddress as Account } from '@nomiclabs/hardhat-ethers/signers'
import { toWei, fromWei } from 'web3-utils'
import { expect } from 'chai'

const getTimestamp = (dates: string): number => {
const date = new Date(dates)
return date.getTime() / 1000
}

const setBlockTimestamp = async (dates: string) => {
await network.provider.send('evm_setNextBlockTimestamp', [getTimestamp(dates)])
}

describe('LOAS', () => {
let loas: Contract
let genesis: Account
let originalClaimer: Account
let allowedClaimer: Account
let invalidClaimer: Account

before(async () => {
const accounts = await ethers.getSigners()
genesis = accounts[1]
originalClaimer = accounts[2]
allowedClaimer = accounts[3]
invalidClaimer = accounts[4]
})

beforeEach(async () => {
await network.provider.send('hardhat_reset')
loas = await (await ethers.getContractFactory('LOAS')).deploy()
})

describe('revoke()', async () => {
const expectBalance = async (user: Account, expOAS: number, expLOAS: number) => {
const actualOAS = fromWei((await user.getBalance()).toString())
const actualLOAS = fromWei((await loas.balanceOf(user.address)).toString())
expect(actualOAS).to.match(new RegExp(`^${expOAS + 10000}`))
expect(actualLOAS).to.match(new RegExp(`^${expLOAS}`))
}
const expectTotalSupply = async (exp: number) => {
const actual = fromWei((await loas.totalSupply()).toString())
expect(actual).to.match(new RegExp(`^${exp}`))
}

it('revoke all', async () => {
// initial balance.
await expectBalance(genesis, 0, 0)
await expectBalance(originalClaimer, 0, 0)
await expectTotalSupply(0)

// minting.
await setBlockTimestamp('2100/01/01')
await loas
.connect(genesis)
.mint(originalClaimer.address, getTimestamp('2100/07/01'), getTimestamp('2100/12/31'), {
value: toWei('100'),
})

// after minted.
await expectBalance(originalClaimer, 0, 100)
await expectTotalSupply(100)

// 1 month elapsed.
await setBlockTimestamp('2100/07/31')
await loas.connect(originalClaimer).claim(toWei('16'))
await expectBalance(originalClaimer, 16, 84)
await expectTotalSupply(84)

// Fail by non-existing holder
let tx = loas.connect(originalClaimer).revoke(invalidClaimer.address, toWei('84'))
await expect(tx).to.revertedWith('NotFound()')

// Fail by invalid revoker.
tx = loas.connect(originalClaimer).revoke(originalClaimer.address, toWei('84'))
await expect(tx).to.revertedWith('InvalidRevoker()')

// Revoke all.
await expect(await loas.connect(genesis).revoke(originalClaimer.address, toWei('84')))
.to.emit(loas, 'Revoke')
.withArgs(originalClaimer.address, originalClaimer.address, toWei('84'))
await expectBalance(genesis, -16, 0)
await expectTotalSupply(0)
})

it('revoke locked only', async () => {
// minting.
await setBlockTimestamp('2100/01/01')
await loas
.connect(genesis)
.mint(originalClaimer.address, getTimestamp('2100/07/01'), getTimestamp('2100/12/31'), {
value: toWei('100'),
})

// after minted.
await expectBalance(originalClaimer, 0, 100)
await expectTotalSupply(100)

// 1 month elapsed.
await setBlockTimestamp('2100/07/31')

// Revoke locked only.
await loas.connect(genesis).revoke(originalClaimer.address, 0)
let actualOAS = fromWei((await genesis.getBalance()).toString())
expect(Math.ceil(Number(actualOAS))).to.equal(10000-100+84)
let supply = fromWei((await loas.totalSupply()).toString())
expect(Math.ceil(Number(supply))).to.equal(17)

// Claim.
await loas.connect(originalClaimer).claim(toWei('16'))
await expectBalance(originalClaimer, 16, 0)
supply = fromWei((await loas.totalSupply()).toString())
expect(Math.ceil(Number(supply))).to.equal(1) // left less than 1 OAS.

// 2 month elapsed.
await setBlockTimestamp('2100/08/31')

// Claim left.
const left = await loas.balanceOf(originalClaimer.address)
await loas.connect(originalClaimer).claim(left)
supply = fromWei((await loas.totalSupply()).toString())
expect(Math.ceil(Number(supply))).to.equal(0)
})

it('revoke splited LOAS', async () => {
await setBlockTimestamp('2100/01/01')
await loas
.connect(genesis)
.mint(originalClaimer.address, getTimestamp('2100/07/01'), getTimestamp('2100/12/31'), {
value: toWei('100', 'ether'),
})

await expectBalance(allowedClaimer, 0, 0)

// 2 month elapsed.
await setBlockTimestamp('2100/08/31')

// transfer.
await loas.connect(genesis)['allow(address,address)'](originalClaimer.address, allowedClaimer.address)
await loas.connect(originalClaimer)['transfer(address,uint256)'](allowedClaimer.address, toWei('50'))
await expectBalance(originalClaimer, 0, 50)
await expectBalance(allowedClaimer, 0, 50)
await expectTotalSupply(100)

// try to revoke original claimer, but it should fail as the locked LOAS is over than original claimer's balance.
const tx = loas.connect(genesis).revoke(originalClaimer.address, 0)
await expect(tx).to.revertedWith('OverAmount()')

// revoke all the original claimer's balance.
await loas.connect(genesis).revoke(originalClaimer.address, toWei('50'))
await expectBalance(originalClaimer, 0, 0)
await expectBalance(genesis, -50, 0)
await expectTotalSupply(50)

// revoke only locked LOAS from allowed claimer.
await loas.connect(genesis).revoke(allowedClaimer.address, 0)
let actualOAS = fromWei((await genesis.getBalance()).toString())
expect(Math.ceil(Number(actualOAS))).to.equal(10000-100+50+17)
let supply = fromWei((await loas.totalSupply()).toString())
expect(Math.ceil(Number(supply))).to.equal(34)
const clainInfo = await loas.claimInfo(originalClaimer.address)
expect(Math.ceil(Number(fromWei((clainInfo.revoked).toString())))).to.equal(50+17)

// claim all the left amount
const left = await loas.balanceOf(allowedClaimer.address)
await loas.connect(allowedClaimer).claim(left)
supply = fromWei((await loas.totalSupply()).toString())
expect(Math.ceil(Number(supply))).to.equal(0)
})
})
})