Skip to content

Commit 98aa45c

Browse files
cairoethernestognw
andauthored
Add ERC20Collateral (#15)
* Implement `ERC20Collateral` * Update tests * Update timestamp type Co-authored-by: Ernesto García <[email protected]> * Implement IERC6372 * Add uint128 constant Co-authored-by: Ernesto García <[email protected]> * Use constant * Use clock --------- Co-authored-by: Ernesto García <[email protected]>
1 parent 4ff12b0 commit 98aa45c

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20, ERC20Collateral} from "../../token/ERC20/extensions/ERC20Collateral.sol";
6+
7+
abstract contract ERC20CollateralMock is ERC20Collateral {
8+
ERC20Collateral.Collateral private _collateral;
9+
10+
constructor(
11+
uint48 liveness_,
12+
string memory name_,
13+
string memory symbol_
14+
) ERC20(name_, symbol_) ERC20Collateral(liveness_) {
15+
_collateral = ERC20Collateral.Collateral({amount: type(uint128).max, timestamp: clock()});
16+
}
17+
18+
function collateral() public view override returns (ERC20Collateral.Collateral memory) {
19+
return _collateral;
20+
}
21+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6+
import {IERC6372} from "@openzeppelin/contracts/interfaces/IERC6372.sol";
7+
8+
/**
9+
* @dev Extension of {ERC20} that limits the supply of tokens based
10+
* on a collateral amount and time-based expiration.
11+
*
12+
* The {collateral} function must be implemented to return the collateral
13+
* data. This function can call external oracles or use any local storage.
14+
*/
15+
abstract contract ERC20Collateral is ERC20, IERC6372 {
16+
// Structure that stores the details of the collateral
17+
struct Collateral {
18+
uint256 amount;
19+
uint48 timestamp;
20+
}
21+
22+
/**
23+
* @dev Liveness duration of collateral, defined in seconds.
24+
*/
25+
uint48 private immutable _liveness;
26+
27+
/**
28+
* @dev Total supply cap has been exceeded.
29+
*/
30+
error ERC20ExceededSupply(uint256 increasedSupply, uint256 cap);
31+
32+
/**
33+
* @dev Collateral amount has expired.
34+
*/
35+
error ERC20ExpiredCollateral(uint48 timestamp, uint48 expiration);
36+
37+
/**
38+
* @dev Sets the value of the `_liveness`. This value is immutable, it can only be
39+
* set once during construction.
40+
*/
41+
constructor(uint48 liveness_) {
42+
_liveness = liveness_;
43+
}
44+
45+
/**
46+
* @dev Returns the minimum liveness duration of collateral.
47+
*/
48+
function liveness() public view virtual returns (uint48) {
49+
return _liveness;
50+
}
51+
52+
/**
53+
* @inheritdoc IERC6372
54+
*/
55+
function clock() public view virtual returns (uint48) {
56+
return uint48(block.timestamp);
57+
}
58+
59+
/**
60+
* @inheritdoc IERC6372
61+
*/
62+
function CLOCK_MODE() public view virtual returns (string memory) {
63+
return "mode=timestamp";
64+
}
65+
66+
/**
67+
* @dev Returns the collateral data of the token.
68+
*/
69+
function collateral() public view virtual returns (Collateral memory);
70+
71+
/**
72+
* @dev See {ERC20-_update}.
73+
*/
74+
function _update(address from, address to, uint256 value) internal virtual override {
75+
super._update(from, to, value);
76+
77+
if (from == address(0)) {
78+
Collateral memory _collateral = collateral();
79+
80+
uint48 expiration = _collateral.timestamp + liveness();
81+
if (expiration < clock()) {
82+
revert ERC20ExpiredCollateral(_collateral.timestamp, expiration);
83+
}
84+
85+
uint256 supply = totalSupply();
86+
if (supply > _collateral.amount) {
87+
revert ERC20ExceededSupply(supply, _collateral.amount);
88+
}
89+
}
90+
}
91+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
const { time } = require('@nomicfoundation/hardhat-network-helpers');
5+
6+
const name = 'My Token';
7+
const symbol = 'MTKN';
8+
const initialSupply = 100n;
9+
10+
async function fixture() {
11+
const [holder, recipient, approved] = await ethers.getSigners();
12+
13+
const token = await ethers.deployContract('$ERC20CollateralMock', [3600, name, symbol]);
14+
await token.$_mint(holder, initialSupply);
15+
16+
return { holder, recipient, approved, token };
17+
}
18+
19+
describe('ERC20Collateral', function () {
20+
beforeEach(async function () {
21+
Object.assign(this, await loadFixture(fixture));
22+
});
23+
24+
describe('amount', function () {
25+
const MAX_UINT128 = 2n ** 128n - 1n;
26+
27+
it('mint all of collateral amount', async function () {
28+
await expect(this.token.$_mint(this.holder, MAX_UINT128 - initialSupply)).to.changeTokenBalance(this.token, this.holder, MAX_UINT128 - initialSupply);
29+
});
30+
31+
it('reverts when minting more than collateral amount', async function () {
32+
await expect(this.token.$_mint(this.holder, MAX_UINT128)).to.be.revertedWithCustomError(this.token, 'ERC20ExceededSupply');
33+
});
34+
});
35+
36+
describe('expiration', function () {
37+
it('mint before expiration', async function () {
38+
await expect(this.token.$_mint(this.holder, initialSupply)).to.changeTokenBalance(this.token, this.holder, initialSupply);
39+
});
40+
41+
it('reverts when minting after expiration', async function () {
42+
await time.increase(await this.token.liveness());
43+
await expect(this.token.$_mint(this.holder, initialSupply)).to.be.revertedWithCustomError(this.token, 'ERC20ExpiredCollateral');
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)