Skip to content

Commit

Permalink
Otoken: Burn and Mint (#31)
Browse files Browse the repository at this point in the history
* Rearrange oz folders and init commit on oToken.sol

* Add descriptions for function parameters

* Add layout for otoken tests

* Remove ERC20 operation from this branch

* Add BokkyPooBahsDateTimeLibrary and convert timestamp to date string.

* Add tests for Otoken.sol.

* Remove unused function in DateTimeLibrary.

* Add comments, test different name/symbol strings

* Add non-eth asset test

* Rearrange test orders and add expiry test for now + 3000 years

* Add 0 expiry and max uint256 expiry test

* Use new name and symbol

* Remove mapping, refactor optino type and month string getters.

* Remove args for getNameAndSymbol function

* Make test coverage 100%

* Add test for init twice with different parameters

* Add mintOtoken and burnOtoken functions and tests

* Add comments

* Rearrange function orders

* Simplify otoken test syntax.

* Add more comments

* Fix typos
  • Loading branch information
antoncoding committed Jul 25, 2020
1 parent 861e75a commit d4ad496
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 21 deletions.
48 changes: 47 additions & 1 deletion contracts/Otoken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ contract Otoken is ERC20Initializable {
__ERC20_init_unchained(name, symbol);
}

/**
* @notice Mint oToken for an account.
* @dev this is a Controller only method. Access control is taken care of by _beforeTokenTransfer hook.
* @param account the account to mint token to
* @param amount the amount to mint
*/
function mintOtoken(address account, uint256 amount) external {
_mint(account, amount);
}

/**
* @notice Burn oToken from an account.
* @dev this is a Controller only method. Access control is taken care of by _beforeTokenTransfer hook.
* @param account the account to burn token from
* @param amount the amount to burn
*/
function burnOtoken(address account, uint256 amount) external {
_burn(account, amount);
}

/**
* @notice generate name and symbol for an option
* @return name ETHUSDC 05-September-2020 200 Put USDC Collateral
Expand Down Expand Up @@ -149,7 +169,7 @@ contract Otoken is ERC20Initializable {
}

/**
* @dev return string of optino type
* @dev return string of option type
* @return symbol P or C
* @return full Put or Call
*/
Expand Down Expand Up @@ -193,4 +213,30 @@ contract Otoken is ERC20Initializable {
return ("DEC", "December");
}
}

/**
* @dev this function overrides the _beforeTokenTransfer hook in ERC20Initializable.sol.
* If the operation is mint or burn, requires msg.sender to be the controller.
* The function signature is the same as _beforeTokenTransfer defined in ERC20Initializable.sol.
* @param from from address
* @param to to address
* @param amount amount to transfer
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override {
if (from == address(0)) {
require(
msg.sender == AddressBookInterface(addressBook).getController(),
"Otoken: Only Controller can mint Otokens."
);
} else if (to == address(0)) {
require(
msg.sender == AddressBookInterface(addressBook).getController(),
"Otoken: Only Controller can burn Otokens."
);
}
}
}
9 changes: 9 additions & 0 deletions contracts/mocks/MockAddressBook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ contract MockAddressBook {
address private _otokenImpl;
address private _whitelist;
address private _otokenFactoryImpl;
address private _controllerImpl;

function setOtokenImpl(address _newImpl) external {
_otokenImpl = _newImpl;
Expand All @@ -26,7 +27,15 @@ contract MockAddressBook {
return _otokenFactoryImpl;
}

function getController() external view returns (address) {
return _controllerImpl;
}

function setOtokenFactory(address _otokenFactory) external {
_otokenFactoryImpl = _otokenFactory;
}

function setController(address _controller) external {
_controllerImpl = _controller;
}
}
124 changes: 104 additions & 20 deletions test/Otoken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ const {expectRevert} = require('@openzeppelin/test-helpers')

const Otoken = artifacts.require('Otoken.sol')
const MockERC20 = artifacts.require('MockERC20.sol')
const MockAddressBook = artifacts.require('MockAddressBook')
const ZERO_ADDR = '0x0000000000000000000000000000000000000000'
const ETH_ADDR = ZERO_ADDR

contract('Otoken', ([deployer, mockAddressBook, random]) => {
contract('Otoken', ([deployer, controller, user1, user2, random]) => {
let otoken: OtokenInstance
let usdc: MockERC20Instance
let mockAddressBookAddr: string

// let expiry: number;
const strikePrice = new BigNumber(200).times(new BigNumber(10).exponentiatedBy(18))
Expand All @@ -19,7 +21,13 @@ contract('Otoken', ([deployer, mockAddressBook, random]) => {

before('Deployment', async () => {
// Need another mock contract for addressbook when we add ERC20 operations.
otoken = await Otoken.new(mockAddressBook)
const addressBook = await MockAddressBook.new()
mockAddressBookAddr = addressBook.address
await addressBook.setController(controller)

// deploy oToken with addressbook
otoken = await Otoken.new(addressBook.address)

usdc = await MockERC20.new('USDC', 'USDC')
})

Expand Down Expand Up @@ -55,23 +63,23 @@ contract('Otoken', ([deployer, mockAddressBook, random]) => {
})

it('should set the right name for calls', async () => {
const call = await Otoken.new(mockAddressBook)
const call = await Otoken.new(mockAddressBookAddr)
await call.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, expiry, false, {from: deployer})
assert.equal(await call.name(), `ETHUSDC 25-September-2020 200Call USDC Collateral`)
assert.equal(await call.symbol(), `oETHUSDC-25SEP20-200C`)
})

it('should set the right name for non-eth options', async () => {
const weth = await MockERC20.new('WETH', 'WETH')
const put = await Otoken.new(mockAddressBook)
const put = await Otoken.new(mockAddressBookAddr)
await put.init(weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, {from: deployer})
assert.equal(await put.name(), `WETHUSDC 25-September-2020 200Put USDC Collateral`)
assert.equal(await put.symbol(), `oWETHUSDC-25SEP20-200P`)
})

it('should revert when init asset with non-erc20 address', async () => {
/* This behavior should've been banned by factory) */
const put = await Otoken.new(mockAddressBook)
const put = await Otoken.new(mockAddressBookAddr)
await expectRevert(
put.init(random, usdc.address, usdc.address, strikePrice, expiry, isPut, {from: deployer}),
'revert',
Expand All @@ -80,15 +88,15 @@ contract('Otoken', ([deployer, mockAddressBook, random]) => {

it('should set the right name for options with 0 expiry (should be banned by factory)', async () => {
/* This behavior should've been banned by factory) */
const otoken = await Otoken.new(mockAddressBook)
const otoken = await Otoken.new(mockAddressBookAddr)
await otoken.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 0, isPut, {from: deployer})
assert.equal(await otoken.name(), `ETHUSDC 01-January-1970 200Put USDC Collateral`)
assert.equal(await otoken.symbol(), `oETHUSDC-01JAN70-200P`)
})

it('should set the right name for options expiry on 2345/12/31', async () => {
/** This is the largest timestamp that the factoy will allow (the largest bokkypoobah covers) **/
const otoken = await Otoken.new(mockAddressBook)
const otoken = await Otoken.new(mockAddressBookAddr)
const _expiry = '11865394800' // Mon, 31 Dec 2345
await otoken.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, _expiry, isPut, {from: deployer})
assert.equal(await otoken.name(), `ETHUSDC 31-December-2345 200Put USDC Collateral`)
Expand All @@ -97,72 +105,148 @@ contract('Otoken', ([deployer, mockAddressBook, random]) => {

it('should set the right name and symbol for expiry on each month', async () => {
// We need to go through all decision branches in _getMonth() to make a 100% test coverage.
const January = await Otoken.new(mockAddressBook)
const January = await Otoken.new(mockAddressBookAddr)
await January.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1893456000, isPut, {from: deployer})
assert.equal(await January.name(), 'ETHUSDC 01-January-2030 200Put USDC Collateral')
assert.equal(await January.symbol(), 'oETHUSDC-01JAN30-200P')

const February = await Otoken.new(mockAddressBook)
const February = await Otoken.new(mockAddressBookAddr)
await February.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1896134400, isPut, {from: deployer})
assert.equal(await February.name(), 'ETHUSDC 01-February-2030 200Put USDC Collateral')
assert.equal(await February.symbol(), 'oETHUSDC-01FEB30-200P')

const March = await Otoken.new(mockAddressBook)
const March = await Otoken.new(mockAddressBookAddr)
await March.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1898553600, isPut, {from: deployer})
assert.equal(await March.name(), 'ETHUSDC 01-March-2030 200Put USDC Collateral')
assert.equal(await March.symbol(), 'oETHUSDC-01MAR30-200P')

const April = await Otoken.new(mockAddressBook)
const April = await Otoken.new(mockAddressBookAddr)
await April.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1901232000, isPut, {from: deployer})
assert.equal(await April.name(), 'ETHUSDC 01-April-2030 200Put USDC Collateral')
assert.equal(await April.symbol(), 'oETHUSDC-01APR30-200P')

const May = await Otoken.new(mockAddressBook)
const May = await Otoken.new(mockAddressBookAddr)
await May.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1903824000, isPut, {from: deployer})
assert.equal(await May.name(), 'ETHUSDC 01-May-2030 200Put USDC Collateral')
assert.equal(await May.symbol(), 'oETHUSDC-01MAY30-200P')

const June = await Otoken.new(mockAddressBook)
const June = await Otoken.new(mockAddressBookAddr)
await June.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1906502400, isPut, {from: deployer})
assert.equal(await June.name(), 'ETHUSDC 01-June-2030 200Put USDC Collateral')
assert.equal(await June.symbol(), 'oETHUSDC-01JUN30-200P')

const July = await Otoken.new(mockAddressBook)
const July = await Otoken.new(mockAddressBookAddr)
await July.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1909094400, isPut, {from: deployer})
assert.equal(await July.name(), 'ETHUSDC 01-July-2030 200Put USDC Collateral')
assert.equal(await July.symbol(), 'oETHUSDC-01JUL30-200P')

const August = await Otoken.new(mockAddressBook)
const August = await Otoken.new(mockAddressBookAddr)
await August.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1911772800, isPut, {from: deployer})
assert.equal(await August.name(), 'ETHUSDC 01-August-2030 200Put USDC Collateral')
assert.equal(await August.symbol(), 'oETHUSDC-01AUG30-200P')

const September = await Otoken.new(mockAddressBook)
const September = await Otoken.new(mockAddressBookAddr)
await September.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1914451200, isPut, {from: deployer})
assert.equal(await September.name(), 'ETHUSDC 01-September-2030 200Put USDC Collateral')
assert.equal(await September.symbol(), 'oETHUSDC-01SEP30-200P')

const October = await Otoken.new(mockAddressBook)
const October = await Otoken.new(mockAddressBookAddr)
await October.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1917043200, isPut, {from: deployer})
assert.equal(await October.name(), 'ETHUSDC 01-October-2030 200Put USDC Collateral')
assert.equal(await October.symbol(), 'oETHUSDC-01OCT30-200P')

const November = await Otoken.new(mockAddressBook)
const November = await Otoken.new(mockAddressBookAddr)
await November.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1919721600, isPut, {from: deployer})
assert.equal(await November.name(), 'ETHUSDC 01-November-2030 200Put USDC Collateral')
assert.equal(await November.symbol(), 'oETHUSDC-01NOV30-200P')

const December = await Otoken.new(mockAddressBook)
const December = await Otoken.new(mockAddressBookAddr)
await December.init(ETH_ADDR, usdc.address, usdc.address, strikePrice, 1922313600, isPut, {from: deployer})
assert.equal(await December.name(), 'ETHUSDC 01-December-2030 200Put USDC Collateral')
assert.equal(await December.symbol(), 'oETHUSDC-01DEC30-200P')
})

it('should display strikePrice as $0 in name and symbol when strikePrice < 10^18', async () => {
const testOtoken = await Otoken.new(mockAddressBook)
const testOtoken = await Otoken.new(mockAddressBookAddr)
await testOtoken.init(ETH_ADDR, usdc.address, usdc.address, 0, expiry, isPut, {from: deployer})
assert.equal(await testOtoken.name(), `ETHUSDC 25-September-2020 0Put USDC Collateral`)
assert.equal(await testOtoken.symbol(), `oETHUSDC-25SEP20-0P`)
})
})

describe('Token operations: Mint', () => {
const amountToMint = new BigNumber(10).times(new BigNumber(10).exponentiatedBy(18))
it('should be able to mint tokens from controller address', async () => {
await otoken.mintOtoken(user1, amountToMint, {from: controller})
const balance = await otoken.balanceOf(user1)
assert.equal(balance.toString(), amountToMint.toString())
})

it('should revert when minting from random address', async () => {
await expectRevert(
otoken.mintOtoken(user1, amountToMint, {from: random}),
'Otoken: Only Controller can mint Otokens.',
)
})

it('should revert when someone try to mint for himself.', async () => {
await expectRevert(
otoken.mintOtoken(user1, amountToMint, {from: user1}),
'Otoken: Only Controller can mint Otokens.',
)
})
})

describe('Token operations: Transfer', () => {
const amountToMint = new BigNumber(10).times(new BigNumber(10).exponentiatedBy(18))
it('should be able to transfer tokens from user 1 to user 2', async () => {
await otoken.transfer(user2, amountToMint, {from: user1})
const balance = await otoken.balanceOf(user2)
assert.equal(balance.toString(), amountToMint.toString())
})

it('should revert when calling transferFrom with no allownace', async () => {
await expectRevert(
otoken.transferFrom(user2, user1, amountToMint, {from: random}),
'ERC20: transfer amount exceeds allowance',
)
})

it('should revert when controller call transferFrom with no allownace', async () => {
await expectRevert(
otoken.transferFrom(user2, user1, amountToMint, {from: controller}),
'ERC20: transfer amount exceeds allowance',
)
})

it('should be able to use transferFrom to transfer token from user2 to user1.', async () => {
await otoken.approve(random, amountToMint, {from: user2})
await otoken.transferFrom(user2, user1, amountToMint, {from: random})
const user2Remaining = await otoken.balanceOf(user2)
assert.equal(user2Remaining.toString(), '0')
})
})

describe('Token operations: Burn', () => {
const amountToMint = new BigNumber(10).times(new BigNumber(10).exponentiatedBy(18))
it('should revert when burning from random address', async () => {
await expectRevert(
otoken.burnOtoken(user1, amountToMint, {from: random}),
'Otoken: Only Controller can burn Otokens.',
)
})

it('should revert when someone trys to burn for himeself', async () => {
await expectRevert(
otoken.burnOtoken(user1, amountToMint, {from: user1}),
'Otoken: Only Controller can burn Otokens.',
)
})

it('should be able to burn tokens from controller address', async () => {
await otoken.burnOtoken(user1, amountToMint, {from: controller})
const balance = await otoken.balanceOf(user1)
assert.equal(balance.toString(), '0')
})
})
})

0 comments on commit d4ad496

Please sign in to comment.