├── .nvmrc ├── .gitattributes ├── .solcover.js ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── nightly.yml ├── contracts ├── interfaces │ ├── IOrchestrator.sol │ ├── IAmpleforth.sol │ └── IAMPL.sol ├── mocks │ ├── MockUFragmentsPolicy.sol │ ├── Mock.sol │ ├── UInt256LibMock.sol │ ├── ConstructorRebaseCallerContract.sol │ ├── RebaseCallerContract.sol │ ├── SelectMock.sol │ ├── GetMedianOracleDataCallerContract.sol │ ├── MockUFragments.sol │ ├── MockOracle.sol │ ├── SafeMathIntMock.sol │ └── MockDownstream.sol ├── lib │ ├── UInt256Lib.sol │ ├── Select.sol │ └── SafeMathInt.sol ├── _external │ ├── IERC20.sol │ ├── ERC20Detailed.sol │ ├── SafeMath.sol │ ├── Ownable.sol │ └── Initializable.sol ├── Orchestrator.sol ├── MedianOracle.sol ├── WAMPL.sol ├── UFragmentsPolicy.sol └── UFragments.sol ├── .solhint.json ├── .prettierrc ├── tsconfig.json ├── test ├── utils │ ├── utils.ts │ └── signatures.ts ├── unit │ ├── UInt256Lib.ts │ ├── Select.ts │ ├── UFragments_erc20_permit.ts │ ├── uFragments_elastic_behavior.ts │ ├── SafeMathInt.ts │ ├── Orchestrator.ts │ ├── UFragments.ts │ ├── MedianOracle.ts │ └── uFragments_erc20_behavior.ts └── simulation │ ├── supply_precision.ts │ └── transfer_precision.ts ├── .gitignore ├── hardhat.config.ts ├── scripts ├── helpers.ts ├── upgrade.ts └── deploy.ts ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ['mocks'], 3 | } 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /contracts/interfaces/IOrchestrator.sol: -------------------------------------------------------------------------------- 1 | // Public interface definition for the Ampleforth Orchestrator on Ethereum (the base-chain) 2 | interface IOrchestrator { 3 | function rebase() external; 4 | } 5 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "not-rely-on-time": "off", 7 | "max-line-length": ["error", 105] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/mocks/MockUFragmentsPolicy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Mock.sol"; 4 | 5 | contract MockUFragmentsPolicy is Mock { 6 | function rebase() external { 7 | emit FunctionCalled("UFragmentsPolicy", "rebase", msg.sender); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/mocks/Mock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | contract Mock { 4 | event FunctionCalled(string instanceName, string functionName, address caller); 5 | event FunctionArguments(uint256[] uintVals, int256[] intVals); 6 | event ReturnValueInt256(int256 val); 7 | event ReturnValueUInt256(uint256 val); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/mocks/UInt256LibMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Mock.sol"; 4 | import "../lib/UInt256Lib.sol"; 5 | 6 | contract UInt256LibMock is Mock { 7 | function toInt256Safe(uint256 a) external returns (int256) { 8 | int256 result = UInt256Lib.toInt256Safe(a); 9 | emit ReturnValueInt256(result); 10 | return result; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/mocks/ConstructorRebaseCallerContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "../Orchestrator.sol"; 4 | 5 | contract ConstructorRebaseCallerContract { 6 | constructor(address orchestrator) public { 7 | // Take out a flash loan. 8 | // Do something funky... 9 | Orchestrator(orchestrator).rebase(); // should fail 10 | // pay back flash loan. 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/mocks/RebaseCallerContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "../Orchestrator.sol"; 4 | 5 | contract RebaseCallerContract { 6 | function callRebase(address orchestrator) public returns (bool) { 7 | // Take out a flash loan. 8 | // Do something funky... 9 | Orchestrator(orchestrator).rebase(); // should fail 10 | // pay back flash loan. 11 | return true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mocks/SelectMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "../lib/Select.sol"; 4 | 5 | contract Mock { 6 | event ReturnValueUInt256(uint256 val); 7 | } 8 | 9 | contract SelectMock is Mock { 10 | function computeMedian(uint256[] calldata data, uint256 size) external returns (uint256) { 11 | uint256 result = Select.computeMedian(data, size); 12 | emit ReturnValueUInt256(result); 13 | return result; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "printWidth": 80, 7 | "overrides": [ 8 | { 9 | "files": "*.sol", 10 | "options": { 11 | "printWidth": 100, 12 | "tabWidth": 4, 13 | "useTabs": false, 14 | "singleQuote": false, 15 | "bracketSpacing": false, 16 | "explicitTypes": "always" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /contracts/interfaces/IAmpleforth.sol: -------------------------------------------------------------------------------- 1 | // Public interface definition for the Ampleforth supply policy on Ethereum (the base-chain) 2 | interface IAmpleforth { 3 | function epoch() external view returns (uint256); 4 | 5 | function lastRebaseTimestampSec() external view returns (uint256); 6 | 7 | function inRebaseWindow() external view returns (bool); 8 | 9 | function globalAmpleforthEpochAndAMPLSupply() external view returns (uint256, uint256); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/lib/UInt256Lib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | /** 5 | * @title Various utilities useful for uint256. 6 | */ 7 | library UInt256Lib { 8 | uint256 private constant MAX_INT256 = ~(uint256(1) << 255); 9 | 10 | /** 11 | * @dev Safely converts a uint256 to an int256. 12 | */ 13 | function toInt256Safe(uint256 a) internal pure returns (int256) { 14 | require(a <= MAX_INT256); 15 | return int256(a); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "downlevelIteration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["./scripts", "./test"], 13 | "files": [ 14 | "hardhat.config.ts", 15 | "./node_modules/@nomiclabs/hardhat-ethers/src/type-extensions.ts", 16 | "./node_modules/@nomiclabs/hardhat-waffle/src/type-extensions.ts", 17 | "./node_modules/@openzeppelin/hardhat-upgrades/src/type-extensions.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /test/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { BigNumberish } from 'ethers' 3 | import { BigNumber as BN } from 'bignumber.js' 4 | 5 | export const imul = (a: BigNumberish, b: BigNumberish, c: BigNumberish) => { 6 | return ethers.BigNumber.from( 7 | new BN(a.toString()).times(b.toString()).idiv(c.toString()).toString(10), 8 | ) 9 | } 10 | 11 | export const increaseTime = async (seconds: BigNumberish) => { 12 | const now = (await ethers.provider.getBlock('latest')).timestamp 13 | await ethers.provider.send('evm_mine', [ 14 | ethers.BigNumber.from(seconds).add(now).toNumber(), 15 | ]) 16 | } 17 | -------------------------------------------------------------------------------- /contracts/mocks/GetMedianOracleDataCallerContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "../MedianOracle.sol"; 4 | 5 | contract GetMedianOracleDataCallerContract { 6 | event ReturnValueUInt256Bool(uint256 value, bool valid); 7 | 8 | IOracle public oracle; 9 | 10 | constructor() public {} 11 | 12 | function setOracle(IOracle _oracle) public { 13 | oracle = _oracle; 14 | } 15 | 16 | function getData() public returns (uint256) { 17 | uint256 _value; 18 | bool _valid; 19 | (_value, _valid) = oracle.getData(); 20 | emit ReturnValueUInt256Bool(_value, _valid); 21 | return _value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | os: [ubuntu-latest] 17 | 18 | steps: 19 | - name: Setup Repo 20 | uses: actions/checkout@v3 21 | 22 | - name: Uses node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Install all workspaces 28 | run: yarn install --immutable 29 | 30 | - name: Seutp workspaces 31 | run: yarn compile 32 | 33 | - name: Lint 34 | run: yarn format && yarn lint 35 | 36 | - name: Test 37 | run: yarn test -------------------------------------------------------------------------------- /contracts/mocks/MockUFragments.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Mock.sol"; 4 | 5 | contract MockUFragments is Mock { 6 | uint256 private _supply; 7 | 8 | // Methods to mock data on the chain 9 | function storeSupply(uint256 supply) public { 10 | _supply = supply; 11 | } 12 | 13 | // Mock methods 14 | function rebase(uint256 epoch, int256 supplyDelta) public returns (uint256) { 15 | emit FunctionCalled("UFragments", "rebase", msg.sender); 16 | uint256[] memory uintVals = new uint256[](1); 17 | uintVals[0] = epoch; 18 | int256[] memory intVals = new int256[](1); 19 | intVals[0] = supplyDelta; 20 | emit FunctionArguments(uintVals, intVals); 21 | return uint256(int256(_supply) + int256(supplyDelta)); 22 | } 23 | 24 | function totalSupply() public view returns (uint256) { 25 | return _supply; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/_external/IERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | /** 4 | * @title ERC20 interface 5 | * @dev see https://github.com/ethereum/EIPs/issues/20 6 | */ 7 | interface IERC20 { 8 | function totalSupply() external view returns (uint256); 9 | 10 | function balanceOf(address who) external view returns (uint256); 11 | 12 | function allowance(address owner, address spender) external view returns (uint256); 13 | 14 | function transfer(address to, uint256 value) external returns (bool); 15 | 16 | function approve(address spender, uint256 value) external returns (bool); 17 | 18 | function transferFrom( 19 | address from, 20 | address to, 21 | uint256 value 22 | ) external returns (bool); 23 | 24 | event Transfer(address indexed from, address indexed to, uint256 value); 25 | 26 | event Approval(address indexed owner, address indexed spender, uint256 value); 27 | } 28 | -------------------------------------------------------------------------------- /contracts/mocks/MockOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Mock.sol"; 4 | 5 | contract MockOracle is Mock { 6 | bool private _validity = true; 7 | uint256 private _data; 8 | string public name; 9 | 10 | constructor(string memory name_) public { 11 | name = name_; 12 | } 13 | 14 | // Mock methods 15 | function getData() external returns (uint256, bool) { 16 | emit FunctionCalled(name, "getData", msg.sender); 17 | uint256[] memory uintVals = new uint256[](0); 18 | int256[] memory intVals = new int256[](0); 19 | emit FunctionArguments(uintVals, intVals); 20 | return (_data, _validity); 21 | } 22 | 23 | // Methods to mock data on the chain 24 | function storeData(uint256 data) public { 25 | _data = data; 26 | } 27 | 28 | function storeValidity(bool validity) public { 29 | _validity = validity; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node-version: [16.x] 15 | os: [ubuntu-latest] 16 | 17 | steps: 18 | - name: Setup Repo 19 | uses: actions/checkout@v3 20 | 21 | - name: Uses node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install all workspaces 27 | run: yarn install --immutable 28 | 29 | - name: Seutp workspaces 30 | run: yarn compile 31 | 32 | - name: Lint 33 | run: yarn format && yarn lint 34 | 35 | - name: Test 36 | run: yarn coverage 37 | 38 | - name: spot-contracts report coverage 39 | uses: coverallsapp/github-action@1.1.3 40 | with: 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | path-to-lcov: "./coverage/lcov.info" -------------------------------------------------------------------------------- /contracts/lib/Select.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | /** 5 | * @title Select 6 | * @dev Median Selection Library 7 | */ 8 | library Select { 9 | /** 10 | * @dev Sorts the input array up to the denoted size, and returns the median. 11 | * @param array Input array to compute its median. 12 | * @param size Number of elements in array to compute the median for. 13 | * @return Median of array. 14 | */ 15 | function computeMedian(uint256[] memory array, uint256 size) internal pure returns (uint256) { 16 | require(size > 0 && array.length >= size); 17 | for (uint256 i = 1; i < size; i++) { 18 | for (uint256 j = i; j > 0 && array[j - 1] > array[j]; j--) { 19 | uint256 tmp = array[j]; 20 | array[j] = array[j - 1]; 21 | array[j - 1] = tmp; 22 | } 23 | } 24 | if (size % 2 == 1) { 25 | return array[size / 2]; 26 | } else { 27 | return (array[size / 2] + array[size / 2 - 1]) / 2; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/_external/ERC20Detailed.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Initializable.sol"; 4 | import "./IERC20.sol"; 5 | 6 | /** 7 | * @title ERC20Detailed token 8 | * @dev The decimals are only for visualization purposes. 9 | * All the operations are done using the smallest and indivisible token unit, 10 | * just as on Ethereum all the operations are done in wei. 11 | */ 12 | abstract contract ERC20Detailed is Initializable, IERC20 { 13 | string private _name; 14 | string private _symbol; 15 | uint8 private _decimals; 16 | 17 | function initialize( 18 | string memory name, 19 | string memory symbol, 20 | uint8 decimals 21 | ) public virtual initializer { 22 | _name = name; 23 | _symbol = symbol; 24 | _decimals = decimals; 25 | } 26 | 27 | /** 28 | * @return the name of the token. 29 | */ 30 | function name() public view returns (string memory) { 31 | return _name; 32 | } 33 | 34 | /** 35 | * @return the symbol of the token. 36 | */ 37 | function symbol() public view returns (string memory) { 38 | return _symbol; 39 | } 40 | 41 | /** 42 | * @return the number of decimals of the token. 43 | */ 44 | function decimals() public view returns (uint8) { 45 | return _decimals; 46 | } 47 | 48 | uint256[50] private ______gap; 49 | } 50 | -------------------------------------------------------------------------------- /contracts/mocks/SafeMathIntMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Mock.sol"; 4 | import "../lib/SafeMathInt.sol"; 5 | 6 | contract SafeMathIntMock is Mock { 7 | function mul(int256 a, int256 b) external returns (int256) { 8 | int256 result = SafeMathInt.mul(a, b); 9 | emit ReturnValueInt256(result); 10 | return result; 11 | } 12 | 13 | function div(int256 a, int256 b) external returns (int256) { 14 | int256 result = SafeMathInt.div(a, b); 15 | emit ReturnValueInt256(result); 16 | return result; 17 | } 18 | 19 | function sub(int256 a, int256 b) external returns (int256) { 20 | int256 result = SafeMathInt.sub(a, b); 21 | emit ReturnValueInt256(result); 22 | return result; 23 | } 24 | 25 | function add(int256 a, int256 b) external returns (int256) { 26 | int256 result = SafeMathInt.add(a, b); 27 | emit ReturnValueInt256(result); 28 | return result; 29 | } 30 | 31 | function abs(int256 a) external returns (int256) { 32 | int256 result = SafeMathInt.abs(a); 33 | emit ReturnValueInt256(result); 34 | return result; 35 | } 36 | 37 | function twoPower(int256 e, int256 one) external returns (int256) { 38 | int256 result = SafeMathInt.twoPower(e, one); 39 | emit ReturnValueInt256(result); 40 | return result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/mocks/MockDownstream.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Mock.sol"; 4 | 5 | contract MockDownstream is Mock { 6 | function updateNoArg() external returns (bool) { 7 | emit FunctionCalled("MockDownstream", "updateNoArg", msg.sender); 8 | uint256[] memory uintVals = new uint256[](0); 9 | int256[] memory intVals = new int256[](0); 10 | emit FunctionArguments(uintVals, intVals); 11 | return true; 12 | } 13 | 14 | function updateOneArg(uint256 u) external { 15 | emit FunctionCalled("MockDownstream", "updateOneArg", msg.sender); 16 | 17 | uint256[] memory uintVals = new uint256[](1); 18 | uintVals[0] = u; 19 | int256[] memory intVals = new int256[](0); 20 | emit FunctionArguments(uintVals, intVals); 21 | } 22 | 23 | function updateTwoArgs(uint256 u, int256 i) external { 24 | emit FunctionCalled("MockDownstream", "updateTwoArgs", msg.sender); 25 | 26 | uint256[] memory uintVals = new uint256[](1); 27 | uintVals[0] = u; 28 | int256[] memory intVals = new int256[](1); 29 | intVals[0] = i; 30 | emit FunctionArguments(uintVals, intVals); 31 | } 32 | 33 | function reverts() external { 34 | emit FunctionCalled("MockDownstream", "reverts", msg.sender); 35 | 36 | uint256[] memory uintVals = new uint256[](0); 37 | int256[] memory intVals = new int256[](0); 38 | emit FunctionArguments(uintVals, intVals); 39 | 40 | require(false, "reverted"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | build 3 | dist 4 | 5 | ### Emacs ## 6 | *~ 7 | \#*\# 8 | .\#* 9 | 10 | ### Vim ## 11 | *.swp 12 | 13 | # 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | # 17 | 18 | # User-specific stuff: 19 | .idea/**/workspace.xml 20 | .idea/**/tasks.xml 21 | .idea/dictionaries 22 | 23 | # Sensitive or high-churn files: 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | 31 | # Gradle: 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # CMake 36 | cmake-build-debug/ 37 | cmake-build-release/ 38 | 39 | # Mongo Explorer plugin: 40 | .idea/**/mongoSettings.xml 41 | 42 | ## File-based project format: 43 | *.iws 44 | 45 | ## Plugin-specific files: 46 | 47 | # IntelliJ 48 | out/ 49 | 50 | # mpeltonen/sbt-idea plugin 51 | .idea_modules/ 52 | 53 | # JIRA plugin 54 | atlassian-ide-plugin.xml 55 | 56 | # Cursive Clojure plugin 57 | .idea/replstate.xml 58 | 59 | # Crashlytics plugin (for Android Studio and IntelliJ) 60 | com_crashlytics_export_strings.xml 61 | crashlytics.properties 62 | crashlytics-build.properties 63 | fabric.properties 64 | 65 | # NodeJS dependencies 66 | node_modules/* 67 | 68 | # ES-Lint 69 | .eslintcache 70 | 71 | # Solidity-Coverage 72 | allFiredEvents 73 | scTopics 74 | scDebugLog 75 | coverage.json 76 | coverage/ 77 | coverageEnv/ 78 | 79 | node_modules 80 | 81 | #Buidler files 82 | cache 83 | artifacts 84 | .openzeppelin 85 | 86 | # env 87 | .env 88 | -------------------------------------------------------------------------------- /contracts/interfaces/IAMPL.sol: -------------------------------------------------------------------------------- 1 | // Public interface definition for the AMPL - ERC20 token on Ethereum (the base-chain) 2 | interface IAMPL { 3 | // ERC20 4 | function totalSupply() external view returns (uint256); 5 | 6 | function balanceOf(address who) external view returns (uint256); 7 | 8 | function allowance(address owner_, address spender) external view returns (uint256); 9 | 10 | function transfer(address to, uint256 value) external returns (bool); 11 | 12 | function approve(address spender, uint256 value) external returns (bool); 13 | 14 | function increaseAllowance(address spender, uint256 addedValue) external returns (bool); 15 | 16 | function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); 17 | 18 | function transferFrom( 19 | address from, 20 | address to, 21 | uint256 value 22 | ) external returns (bool); 23 | 24 | // EIP-2612 25 | function permit( 26 | address owner, 27 | address spender, 28 | uint256 value, 29 | uint256 deadline, 30 | uint8 v, 31 | bytes32 r, 32 | bytes32 s 33 | ) external; 34 | 35 | function nonces(address owner) external view returns (uint256); 36 | 37 | function DOMAIN_SEPARATOR() external view returns (bytes32); 38 | 39 | // Elastic token interface 40 | function scaledBalanceOf(address who) external view returns (uint256); 41 | 42 | function scaledTotalSupply() external view returns (uint256); 43 | 44 | function transferAll(address to) external returns (bool); 45 | 46 | function transferAllFrom(address from, address to) external returns (bool); 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/UInt256Lib.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { Contract } from 'ethers' 3 | import { expect } from 'chai' 4 | 5 | describe('UInt256Lib', () => { 6 | const MAX_INT256 = ethers.BigNumber.from(2).pow(255).sub(1) 7 | 8 | let UInt256Lib: Contract 9 | 10 | beforeEach(async function () { 11 | // deploy contract 12 | const factory = await ethers.getContractFactory('UInt256LibMock') 13 | UInt256Lib = await factory.deploy() 14 | await UInt256Lib.deployed() 15 | }) 16 | 17 | describe('toInt256Safe', function () { 18 | describe('when then number is more than MAX_INT256', () => { 19 | it('should fail', async function () { 20 | await expect(UInt256Lib.toInt256Safe(MAX_INT256.add(1))).to.be.reverted 21 | }) 22 | }) 23 | 24 | describe('when then number is MAX_INT256', () => { 25 | it('converts int to uint256 safely', async function () { 26 | await expect(UInt256Lib.toInt256Safe(MAX_INT256)) 27 | .to.emit(UInt256Lib, 'ReturnValueInt256') 28 | .withArgs(MAX_INT256) 29 | }) 30 | }) 31 | 32 | describe('when then number is less than MAX_INT256', () => { 33 | it('converts int to uint256 safely', async function () { 34 | await expect(UInt256Lib.toInt256Safe(MAX_INT256.sub(1))) 35 | .to.emit(UInt256Lib, 'ReturnValueInt256') 36 | .withArgs(MAX_INT256.sub(1)) 37 | }) 38 | }) 39 | 40 | describe('when then number is 0', () => { 41 | it('converts int to uint256 safely', async function () { 42 | await expect(UInt256Lib.toInt256Safe(0)) 43 | .to.emit(UInt256Lib, 'ReturnValueInt256') 44 | .withArgs(0) 45 | }) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/simulation/supply_precision.ts: -------------------------------------------------------------------------------- 1 | /* 2 | In this script, 3 | During every iteration: 4 | * We double the total fragments supply. 5 | * We test the following guarantee: 6 | - the difference in totalSupply() before and after the rebase(+1) should be exactly 1. 7 | */ 8 | 9 | import { ethers, upgrades } from 'hardhat' 10 | import { expect } from 'chai' 11 | 12 | async function exec() { 13 | const [deployer] = await ethers.getSigners() 14 | const factory = await ethers.getContractFactory('UFragments') 15 | const uFragments = await upgrades.deployProxy( 16 | factory.connect(deployer), 17 | [await deployer.getAddress()], 18 | { 19 | initializer: 'initialize(address)', 20 | }, 21 | ) 22 | await uFragments.connect(deployer).setMonetaryPolicy(deployer.getAddress()) 23 | 24 | const endSupply = ethers.BigNumber.from(2).pow(128).sub(1) 25 | let preRebaseSupply = ethers.BigNumber.from(0), 26 | postRebaseSupply = ethers.BigNumber.from(0) 27 | 28 | let i = 0 29 | do { 30 | console.log('Iteration', i + 1) 31 | 32 | preRebaseSupply = await uFragments.totalSupply() 33 | await uFragments.connect(deployer).rebase(2 * i, 1) 34 | postRebaseSupply = await uFragments.totalSupply() 35 | console.log('Rebased by 1 AMPL') 36 | console.log('Total supply is now', postRebaseSupply.toString(), 'AMPL') 37 | 38 | console.log('Testing precision of supply') 39 | expect(postRebaseSupply.sub(preRebaseSupply).toNumber()).to.eq(1) 40 | 41 | console.log('Doubling supply') 42 | await uFragments.connect(deployer).rebase(2 * i + 1, postRebaseSupply) 43 | i++ 44 | } while ((await uFragments.totalSupply()).lt(endSupply)) 45 | } 46 | 47 | describe('Supply Precision', function () { 48 | it('should successfully run simulation', async function () { 49 | await exec() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config' 2 | import { Wallet } from 'ethers' 3 | 4 | import '@nomiclabs/hardhat-ethers' 5 | import '@nomiclabs/hardhat-waffle' 6 | import '@openzeppelin/hardhat-upgrades' 7 | import '@nomiclabs/hardhat-etherscan' 8 | import 'solidity-coverage' 9 | import 'hardhat-gas-reporter' 10 | 11 | // Loads env variables from .env file 12 | import * as dotenv from 'dotenv' 13 | dotenv.config() 14 | 15 | // load custom tasks 16 | require('./scripts/deploy') 17 | require('./scripts/upgrade') 18 | 19 | export default { 20 | etherscan: { 21 | apiKey: process.env.ETHERSCAN_API_KEY, 22 | }, 23 | networks: { 24 | hardhat: { 25 | initialBaseFeePerGas: 0, 26 | accounts: { 27 | mnemonic: Wallet.createRandom().mnemonic.phrase, 28 | }, 29 | }, 30 | ganache: { 31 | url: 'http://127.0.0.1:8545', 32 | }, 33 | goerli: { 34 | url: `https://eth-goerli.g.alchemy.com/v2/${process.env.ALCHEMY_SECRET}`, 35 | accounts: { 36 | mnemonic: 37 | process.env.PROD_MNEMONIC || Wallet.createRandom().mnemonic.phrase, 38 | }, 39 | minGasPrice: 10000000000, 40 | gasMultiplier: 1.5, 41 | }, 42 | mainnet: { 43 | url: `https://mainnet.infura.io/v3/${process.env.INFURA_SECRET}`, 44 | accounts: { 45 | mnemonic: 46 | process.env.PROD_MNEMONIC || Wallet.createRandom().mnemonic.phrase, 47 | }, 48 | gasMultiplier: 1.05, 49 | }, 50 | }, 51 | solidity: { 52 | compilers: [ 53 | { 54 | version: '0.8.4', 55 | settings: { 56 | optimizer: { 57 | enabled: true, 58 | runs: 200, 59 | }, 60 | }, 61 | }, 62 | ], 63 | }, 64 | mocha: { 65 | timeout: 100000, 66 | bail: true, 67 | }, 68 | gasReporter: { 69 | currency: 'USD', 70 | enabled: process.env.REPORT_GAS ? true : false, 71 | excludeContracts: ['mocks/'], 72 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 73 | }, 74 | } as HardhatUserConfig 75 | -------------------------------------------------------------------------------- /contracts/_external/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | /** 4 | * @title SafeMath 5 | * @dev Math operations with safety checks that revert on error 6 | */ 7 | library SafeMath { 8 | /** 9 | * @dev Multiplies two numbers, reverts on overflow. 10 | */ 11 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 12 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 13 | // benefit is lost if 'b' is also tested. 14 | // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 15 | if (a == 0) { 16 | return 0; 17 | } 18 | 19 | uint256 c = a * b; 20 | require(c / a == b); 21 | 22 | return c; 23 | } 24 | 25 | /** 26 | * @dev Integer division of two numbers truncating the quotient, reverts on division by zero. 27 | */ 28 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 29 | require(b > 0); // Solidity only automatically asserts when dividing by 0 30 | uint256 c = a / b; 31 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 32 | 33 | return c; 34 | } 35 | 36 | /** 37 | * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend). 38 | */ 39 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 40 | require(b <= a); 41 | uint256 c = a - b; 42 | 43 | return c; 44 | } 45 | 46 | /** 47 | * @dev Adds two numbers, reverts on overflow. 48 | */ 49 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 50 | uint256 c = a + b; 51 | require(c >= a); 52 | 53 | return c; 54 | } 55 | 56 | /** 57 | * @dev Divides two numbers and returns the remainder (unsigned integer modulo), 58 | * reverts when dividing by zero. 59 | */ 60 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 61 | require(b != 0); 62 | return a % b; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/_external/Ownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | import "./Initializable.sol"; 4 | 5 | /** 6 | * @title Ownable 7 | * @dev The Ownable contract has an owner address, and provides basic authorization control 8 | * functions, this simplifies the implementation of "user permissions". 9 | */ 10 | contract Ownable is Initializable { 11 | address private _owner; 12 | 13 | event OwnershipRenounced(address indexed previousOwner); 14 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 15 | 16 | /** 17 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 18 | * account. 19 | */ 20 | function initialize(address sender) public virtual initializer { 21 | _owner = sender; 22 | } 23 | 24 | /** 25 | * @return the address of the owner. 26 | */ 27 | function owner() public view returns (address) { 28 | return _owner; 29 | } 30 | 31 | /** 32 | * @dev Throws if called by any account other than the owner. 33 | */ 34 | modifier onlyOwner() { 35 | require(isOwner()); 36 | _; 37 | } 38 | 39 | /** 40 | * @return true if `msg.sender` is the owner of the contract. 41 | */ 42 | function isOwner() public view returns (bool) { 43 | return msg.sender == _owner; 44 | } 45 | 46 | /** 47 | * @dev Allows the current owner to relinquish control of the contract. 48 | * @notice Renouncing to ownership will leave the contract without an owner. 49 | * It will not be possible to call the functions with the `onlyOwner` 50 | * modifier anymore. 51 | */ 52 | function renounceOwnership() public onlyOwner { 53 | emit OwnershipRenounced(_owner); 54 | _owner = address(0); 55 | } 56 | 57 | /** 58 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 59 | * @param newOwner The address to transfer ownership to. 60 | */ 61 | function transferOwnership(address newOwner) public onlyOwner { 62 | _transferOwnership(newOwner); 63 | } 64 | 65 | /** 66 | * @dev Transfers control of the contract to a newOwner. 67 | * @param newOwner The address to transfer ownership to. 68 | */ 69 | function _transferOwnership(address newOwner) internal { 70 | require(newOwner != address(0)); 71 | emit OwnershipTransferred(_owner, newOwner); 72 | _owner = newOwner; 73 | } 74 | 75 | uint256[50] private ______gap; 76 | } 77 | -------------------------------------------------------------------------------- /contracts/_external/Initializable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.4; 2 | 3 | /** 4 | * @title Initializable 5 | * 6 | * @dev Helper contract to support initializer functions. To use it, replace 7 | * the constructor with a function that has the `initializer` modifier. 8 | * WARNING: Unlike constructors, initializer functions must be manually 9 | * invoked. This applies both to deploying an Initializable contract, as well 10 | * as extending an Initializable contract via inheritance. 11 | * WARNING: When used with inheritance, manual care must be taken to not invoke 12 | * a parent initializer twice, or ensure that all initializers are idempotent, 13 | * because this is not dealt with automatically as with constructors. 14 | */ 15 | contract Initializable { 16 | /** 17 | * @dev Indicates that the contract has been initialized. 18 | */ 19 | bool private initialized; 20 | 21 | /** 22 | * @dev Indicates that the contract is in the process of being initialized. 23 | */ 24 | bool private initializing; 25 | 26 | /** 27 | * @dev Modifier to use in the initializer function of a contract. 28 | */ 29 | modifier initializer() { 30 | require( 31 | initializing || isConstructor() || !initialized, 32 | "Contract instance has already been initialized" 33 | ); 34 | 35 | bool wasInitializing = initializing; 36 | initializing = true; 37 | initialized = true; 38 | 39 | _; 40 | 41 | initializing = wasInitializing; 42 | } 43 | 44 | /// @dev Returns true if and only if the function is running in the constructor 45 | function isConstructor() private view returns (bool) { 46 | // extcodesize checks the size of the code stored in an address, and 47 | // address returns the current address. Since the code is still not 48 | // deployed when running a constructor, any checks on its code size will 49 | // yield zero, making it an effective way to detect if a contract is 50 | // under construction or not. 51 | 52 | // MINOR CHANGE HERE: 53 | 54 | // previous code 55 | // uint256 cs; 56 | // assembly { cs := extcodesize(address) } 57 | // return cs == 0; 58 | 59 | // current code 60 | address _self = address(this); 61 | uint256 cs; 62 | assembly { 63 | cs := extcodesize(_self) 64 | } 65 | return cs == 0; 66 | } 67 | 68 | // Reserved storage space to allow for layout changes in the future. 69 | uint256[50] private ______gap; 70 | } 71 | -------------------------------------------------------------------------------- /scripts/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { readFileSync } from 'fs' 3 | import { Signer } from 'ethers' 4 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 5 | import { TransactionResponse } from '@ethersproject/providers' 6 | import { ContractFactory, Contract } from 'ethers' 7 | 8 | const EXTERNAL_ARTIFACTS_PATH = path.join(__dirname, '/../external-artifacts') 9 | export async function getContractFactoryFromExternalArtifacts( 10 | hre: HardhatRuntimeEnvironment, 11 | name: string, 12 | ): Promise { 13 | const artifact = JSON.parse( 14 | readFileSync(`${EXTERNAL_ARTIFACTS_PATH}/${name}.json`).toString(), 15 | ) 16 | return hre.ethers.getContractFactoryFromArtifact(artifact) 17 | } 18 | 19 | export async function waitFor(tx: TransactionResponse) { 20 | return (await tx).wait() 21 | } 22 | 23 | export async function deployContract( 24 | hre: HardhatRuntimeEnvironment, 25 | factoryName: string, 26 | signer: Signer, 27 | params: any = [], 28 | ): Promise { 29 | const contract = await (await hre.ethers.getContractFactory(factoryName)) 30 | .connect(signer) 31 | .deploy(...params) 32 | await contract.deployed() 33 | return contract 34 | } 35 | 36 | export async function deployProxy( 37 | hre: HardhatRuntimeEnvironment, 38 | factoryName: string, 39 | signer: Signer, 40 | initializer: string, 41 | params: any = [], 42 | ): Promise { 43 | const contract = await hre.upgrades.deployProxy( 44 | (await hre.ethers.getContractFactory(factoryName)).connect(signer), 45 | params, 46 | { initializer }, 47 | ) 48 | await contract.deployed() 49 | return contract 50 | } 51 | 52 | export async function deployExternalArtifact( 53 | hre: HardhatRuntimeEnvironment, 54 | name: string, 55 | signer: Signer, 56 | params: any = [], 57 | ): Promise { 58 | const Factory = await getContractFactoryFromExternalArtifacts(hre, name) 59 | const contract = await Factory.connect(signer).deploy(...params) 60 | await contract.deployed() 61 | return contract 62 | } 63 | 64 | export async function verify( 65 | hre: HardhatRuntimeEnvironment, 66 | address: string, 67 | constructorArguments: any = [], 68 | ) { 69 | try { 70 | await hre.run('verify:verify', { address, constructorArguments }) 71 | } catch (e) { 72 | console.log('Verification failed:', e) 73 | console.log('Execute the following') 74 | console.log( 75 | `yarn hardhat run verify:verify --address ${address} --constructor-arguments "${JSON.stringify( 76 | constructorArguments, 77 | ).replace(/"/g, '\\"')}"`, 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uFragments", 3 | "version": "0.0.1", 4 | "description": "Ampleforth protocol smart contracts on Ethereum.", 5 | "keywords": [ 6 | "ethereum", 7 | "smart-contracts", 8 | "solidity" 9 | ], 10 | "homepage": "https://github.com/ampleforth/uFragments#readme", 11 | "bugs": { 12 | "url": "https://github.com/ampleforth/uFragments/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/ampleforth/uFragments.git" 17 | }, 18 | "license": "ISC", 19 | "author": "dev-support@ampleforth.org", 20 | "scripts": { 21 | "compile": "yarn hardhat compile", 22 | "coverage": "yarn hardhat coverage --testfiles 'test/unit/*.ts'", 23 | "format": "yarn prettier --config .prettierrc --write '*.ts' '**/**/*.ts' 'contracts/**/*.sol'", 24 | "lint": "yarn format && yarn solhint 'contracts/**/*.sol'", 25 | "profile": "REPORT_GAS=true yarn hardhat test test/unit/*.ts", 26 | "test": "yarn hardhat test" 27 | }, 28 | "pre-commit": [ 29 | "format", 30 | "lint" 31 | ], 32 | "dependencies": { 33 | "@openzeppelin/contracts-upgradeable": "^4.7.3" 34 | }, 35 | "devDependencies": { 36 | "@ethersproject/abi": "^5.6.4", 37 | "@ethersproject/bytes": "^5.6.1", 38 | "@ethersproject/providers": "^5.6.8", 39 | "@nomiclabs/hardhat-ethers": "^2.1.0", 40 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 41 | "@nomiclabs/hardhat-waffle": "^2.0.3", 42 | "@openzeppelin/hardhat-upgrades": "^1.19.0", 43 | "@openzeppelin/upgrades-core": "^1.19.1", 44 | "@typechain/ethers-v5": "^10.1.0", 45 | "@typechain/hardhat": "^6.1.2", 46 | "@types/chai": "^4.3.1", 47 | "@types/mocha": "^9.1.1", 48 | "@types/node": "^18.6.1", 49 | "@typescript-eslint/eslint-plugin": "^5.0.0", 50 | "@typescript-eslint/parser": "^5.0.0", 51 | "chai": "^4.3.6", 52 | "dotenv": "^16.0.1", 53 | "eslint": "^8.20.0", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-config-standard": "^17.0.0", 56 | "eslint-plugin-import": "^2.26.0", 57 | "eslint-plugin-n": "^15.2.4", 58 | "eslint-plugin-node": "^11.1.0", 59 | "eslint-plugin-prettier": "^4.2.1", 60 | "eslint-plugin-promise": "^6.0.0", 61 | "ethereum-waffle": "^3.4.4", 62 | "ethers": "^5.6.9", 63 | "hardhat": "^2.11.2", 64 | "hardhat-gas-reporter": "^1.0.8", 65 | "lodash": "^4.17.21", 66 | "prettier": "^2.7.1", 67 | "prettier-plugin-solidity": "^1.0.0-dev.23", 68 | "solhint": "^3.3.7", 69 | "solhint-plugin-prettier": "^0.0.5", 70 | "solidity-coverage": "^0.7.21", 71 | "stochasm": "^0.5.0", 72 | "ts-node": "^10.9.1", 73 | "typechain": "^8.1.0", 74 | "typescript": "^4.7.4" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/unit/Select.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { Contract } from 'ethers' 3 | import { expect } from 'chai' 4 | 5 | describe('Select', () => { 6 | let select: Contract 7 | 8 | beforeEach(async function () { 9 | const factory = await ethers.getContractFactory('SelectMock') 10 | select = await factory.deploy() 11 | await select.deployed() 12 | }) 13 | 14 | describe('computeMedian', function () { 15 | it('median of 1', async function () { 16 | const a = ethers.BigNumber.from(5678) 17 | await expect(select.computeMedian([a], 1)) 18 | .to.emit(select, 'ReturnValueUInt256') 19 | .withArgs(a) 20 | }) 21 | 22 | it('median of 2', async function () { 23 | const list = [ethers.BigNumber.from(10000), ethers.BigNumber.from(30000)] 24 | await expect(select.computeMedian(list, 2)) 25 | .to.emit(select, 'ReturnValueUInt256') 26 | .withArgs(20000) 27 | }) 28 | 29 | it('median of 3', async function () { 30 | const list = [ 31 | ethers.BigNumber.from(10000), 32 | ethers.BigNumber.from(30000), 33 | ethers.BigNumber.from(21000), 34 | ] 35 | await expect(select.computeMedian(list, 3)) 36 | .to.emit(select, 'ReturnValueUInt256') 37 | .withArgs(21000) 38 | }) 39 | 40 | it('median of odd sized list', async function () { 41 | const count = 15 42 | const list = Array.from({ length: count }, () => 43 | Math.floor(Math.random() * 10 ** 18), 44 | ) 45 | const median = ethers.BigNumber.from( 46 | [...list].sort((a, b) => b - a)[Math.floor(count / 2)].toString(), 47 | ) 48 | const bn_list = Array.from(list, (x) => 49 | ethers.BigNumber.from(x.toString()), 50 | ) 51 | await expect(select.computeMedian(bn_list, count)) 52 | .to.emit(select, 'ReturnValueUInt256') 53 | .withArgs(median) 54 | }) 55 | 56 | it('median of even sized list', async function () { 57 | const count = 20 58 | const list = Array.from({ length: count }, () => 59 | Math.floor(Math.random() * 10 ** 18), 60 | ) 61 | const bn_list = Array.from(list, (x) => 62 | ethers.BigNumber.from(x.toString()), 63 | ) 64 | list.sort((a, b) => b - a) 65 | let median = ethers.BigNumber.from(list[Math.floor(count / 2)].toString()) 66 | median = median.add( 67 | ethers.BigNumber.from(list[Math.floor(count / 2) - 1].toString()), 68 | ) 69 | median = median.div(2) 70 | 71 | await expect(select.computeMedian(bn_list, count)) 72 | .to.emit(select, 'ReturnValueUInt256') 73 | .withArgs(median) 74 | }) 75 | 76 | it('not enough elements in array', async function () { 77 | await expect(select.computeMedian([1], 2)).to.be.reverted 78 | }) 79 | 80 | it('median of empty list', async function () { 81 | await expect(select.computeMedian([], 1)).to.be.reverted 82 | }) 83 | 84 | it('median of list of size 0', async function () { 85 | await expect(select.computeMedian([10000], 0)).to.be.reverted 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /scripts/upgrade.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import { getAdminAddress } from '@openzeppelin/upgrades-core' 3 | import { Interface } from '@ethersproject/abi' 4 | import { TransactionReceipt } from '@ethersproject/providers' 5 | import ProxyAdmin from '@openzeppelin/upgrades-core/artifacts/ProxyAdmin.json' 6 | 7 | import { getContractFactoryFromExternalArtifacts } from './helpers' 8 | 9 | const parseEvents = ( 10 | receipt: TransactionReceipt, 11 | contractInterface: Interface, 12 | eventName: string, 13 | ) => 14 | receipt.logs 15 | .map((log) => contractInterface.parseLog(log)) 16 | .filter((log) => log.name === eventName) 17 | 18 | task('check:admin', 'Upgrade ampleforth contracts') 19 | .addParam('address', 'proxy address') 20 | .setAction(async (args, hre) => { 21 | console.log(await getAdminAddress(hre.ethers.provider, args.address)) 22 | }) 23 | 24 | task('upgrade:ampl', 'Upgrade ampleforth contracts') 25 | .addParam('contract', 'which implementation contract to use') 26 | .addParam('address', 'which proxy address to upgrade') 27 | .addOptionalParam('multisig', 'which multisig address to use for upgrade') 28 | .setAction(async (args, hre) => { 29 | console.log(args) 30 | const upgrades = hre.upgrades as any 31 | 32 | // can only upgrade token or policy 33 | const supported = ['UFragments', 'UFragmentsPolicy'] 34 | if (!supported.includes(args.contract)) { 35 | throw new Error( 36 | `requested to upgrade ${args.contract} but only ${supported} are supported`, 37 | ) 38 | } 39 | 40 | // get signers 41 | const deployer = (await hre.ethers.getSigners())[0] 42 | console.log('Deployer', await deployer.getAddress()) 43 | 44 | if (args.multisig) { 45 | // deploy new implementation 46 | const implementation = await upgrades.prepareUpgrade( 47 | args.address, 48 | await hre.ethers.getContractFactory(args.contract), 49 | ) 50 | console.log( 51 | `New implementation for ${args.contract} deployed to`, 52 | implementation, 53 | ) 54 | 55 | // prepare upgrade transaction 56 | const admin = new hre.ethers.Contract( 57 | await getAdminAddress(hre.ethers.provider, args.address), 58 | ProxyAdmin.abi, 59 | deployer, 60 | ) 61 | const upgradeTx = await admin.populateTransaction.upgrade( 62 | args.address, 63 | implementation, 64 | ) 65 | console.log(`Upgrade transaction`, upgradeTx) 66 | 67 | // send upgrade transaction to multisig 68 | const MultisigWallet = await getContractFactoryFromExternalArtifacts( 69 | hre, 70 | 'MultiSigWallet', 71 | ) 72 | const multisig = (await MultisigWallet.attach(args.multisig)).connect( 73 | deployer, 74 | ) 75 | 76 | const receipt = await ( 77 | await multisig.submitTransaction( 78 | upgradeTx.to, 79 | upgradeTx.value, 80 | upgradeTx.data, 81 | ) 82 | ).wait() 83 | const events = parseEvents(receipt, multisig.interface, 'Submission') 84 | console.log( 85 | `Upgrade transaction submitted to multisig with transaction index`, 86 | events[0].args.transactionId, 87 | ) 88 | } else { 89 | await upgrades.upgradeProxy( 90 | args.address, 91 | await hre.ethers.getContractFactory(args.contract), 92 | ) 93 | console.log(args.contract, 'upgraded') 94 | } 95 | }) 96 | -------------------------------------------------------------------------------- /test/utils/signatures.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/albertocuestacanada/ERC20Permit/blob/master/utils/signatures.ts 2 | import { 3 | keccak256, 4 | defaultAbiCoder, 5 | toUtf8Bytes, 6 | solidityPack, 7 | splitSignature, 8 | } from 'ethers/lib/utils' 9 | import { ecsign } from 'ethereumjs-util' 10 | import { BigNumberish, Wallet } from 'ethers' 11 | 12 | export const EIP712_DOMAIN_TYPEHASH = keccak256( 13 | toUtf8Bytes( 14 | 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)', 15 | ), 16 | ) 17 | 18 | export const EIP712_DOMAIN_TYPE = [ 19 | { name: 'name', type: 'string' }, 20 | { name: 'version', type: 'string' }, 21 | { name: 'chainId', type: 'uint256' }, 22 | { name: 'verifyingContract', type: 'address' }, 23 | ] 24 | 25 | export const EIP2612_PERMIT_TYPEHASH = keccak256( 26 | toUtf8Bytes( 27 | 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)', 28 | ), 29 | ) 30 | 31 | export const EIP2612_PERMIT_TYPE = [ 32 | { name: 'owner', type: 'address' }, 33 | { name: 'spender', type: 'address' }, 34 | { name: 'value', type: 'uint256' }, 35 | { name: 'nonce', type: 'uint256' }, 36 | { name: 'deadline', type: 'uint256' }, 37 | ] 38 | 39 | // Gets the EIP712 domain separator 40 | export function getDomainSeparator( 41 | version: string, 42 | name: string, 43 | contractAddress: string, 44 | chainId: number, 45 | ) { 46 | return keccak256( 47 | defaultAbiCoder.encode( 48 | ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], 49 | [ 50 | EIP712_DOMAIN_TYPEHASH, 51 | keccak256(toUtf8Bytes(name)), 52 | keccak256(toUtf8Bytes(version)), 53 | chainId, 54 | contractAddress, 55 | ], 56 | ), 57 | ) 58 | } 59 | 60 | // Returns the EIP712 hash which should be signed by the user 61 | // in order to make a call to `permit` 62 | export function getPermitDigest( 63 | version: string, 64 | name: string, 65 | address: string, 66 | chainId: number, 67 | owner: string, 68 | spender: string, 69 | value: number, 70 | nonce: number, 71 | deadline: BigNumberish, 72 | ) { 73 | const DOMAIN_SEPARATOR = getDomainSeparator(version, name, address, chainId) 74 | const permitHash = keccak256( 75 | defaultAbiCoder.encode( 76 | ['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'], 77 | [EIP2612_PERMIT_TYPEHASH, owner, spender, value, nonce, deadline], 78 | ), 79 | ) 80 | const hash = keccak256( 81 | solidityPack( 82 | ['bytes1', 'bytes1', 'bytes32', 'bytes32'], 83 | ['0x19', '0x01', DOMAIN_SEPARATOR, permitHash], 84 | ), 85 | ) 86 | return hash 87 | } 88 | 89 | export const signEIP712Permission = async ( 90 | version: string, 91 | name: string, 92 | verifyingContract: string, 93 | chainId: number, 94 | signer: Wallet, 95 | owner: string, 96 | spender: string, 97 | value: number, 98 | nonce: number, 99 | deadline: BigNumberish, 100 | ) => { 101 | const domain = { 102 | name, 103 | version, 104 | chainId, 105 | verifyingContract, 106 | } 107 | 108 | const types = { Permit: EIP2612_PERMIT_TYPE } 109 | 110 | const data = { 111 | owner, 112 | spender, 113 | value, 114 | nonce, 115 | deadline, 116 | } 117 | 118 | const signature = await signer._signTypedData(domain, types, data) 119 | 120 | return splitSignature(signature) 121 | } 122 | -------------------------------------------------------------------------------- /contracts/Orchestrator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | import "./_external/Ownable.sol"; 5 | 6 | interface IUFragmentsPolicy { 7 | function rebase() external; 8 | } 9 | 10 | /** 11 | * @title Orchestrator 12 | * @notice The orchestrator is the main entry point for rebase operations. It coordinates the policy 13 | * actions with external consumers. 14 | */ 15 | contract Orchestrator is Ownable { 16 | struct Transaction { 17 | bool enabled; 18 | address destination; 19 | bytes data; 20 | } 21 | 22 | // Stable ordering is not guaranteed. 23 | Transaction[] public transactions; 24 | 25 | IUFragmentsPolicy public policy; 26 | 27 | /** 28 | * @param policy_ Address of the UFragments policy. 29 | */ 30 | constructor(address policy_) public { 31 | Ownable.initialize(msg.sender); 32 | policy = IUFragmentsPolicy(policy_); 33 | } 34 | 35 | /** 36 | * @notice Main entry point to initiate a rebase operation. 37 | * The Orchestrator calls rebase on the policy and notifies downstream applications. 38 | * Contracts are guarded from calling, to avoid flash loan attacks on liquidity 39 | * providers. 40 | * If a transaction in the transaction list fails, Orchestrator will stop execution 41 | * and revert to prevent a gas underprice attack. 42 | */ 43 | function rebase() external { 44 | require(msg.sender == tx.origin); // solhint-disable-line avoid-tx-origin 45 | 46 | policy.rebase(); 47 | 48 | for (uint256 i = 0; i < transactions.length; i++) { 49 | Transaction storage t = transactions[i]; 50 | if (t.enabled) { 51 | (bool result, ) = t.destination.call(t.data); 52 | if (!result) { 53 | revert("Transaction Failed"); 54 | } 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * @notice Adds a transaction that gets called for a downstream receiver of rebases 61 | * @param destination Address of contract destination 62 | * @param data Transaction data payload 63 | */ 64 | function addTransaction(address destination, bytes memory data) external onlyOwner { 65 | transactions.push(Transaction({enabled: true, destination: destination, data: data})); 66 | } 67 | 68 | /** 69 | * @param index Index of transaction to remove. 70 | * Transaction ordering may have changed since adding. 71 | */ 72 | function removeTransaction(uint256 index) external onlyOwner { 73 | require(index < transactions.length, "index out of bounds"); 74 | 75 | if (index < transactions.length - 1) { 76 | transactions[index] = transactions[transactions.length - 1]; 77 | } 78 | 79 | transactions.pop(); 80 | } 81 | 82 | /** 83 | * @param index Index of transaction. Transaction ordering may have changed since adding. 84 | * @param enabled True for enabled, false for disabled. 85 | */ 86 | function setTransactionEnabled(uint256 index, bool enabled) external onlyOwner { 87 | require(index < transactions.length, "index must be in range of stored tx list"); 88 | transactions[index].enabled = enabled; 89 | } 90 | 91 | /** 92 | * @return Number of transactions, both enabled and disabled, in transactions list. 93 | */ 94 | function transactionsSize() external view returns (uint256) { 95 | return transactions.length; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ampleforth 2 | 3 | [![Nightly](https://github.com/ampleforth/ampleforth-contracts/actions/workflows/nightly.yml/badge.svg)](https://github.com/ampleforth/ampleforth-contracts/actions/workflows/nightly.yml)  [![Coverage Status](https://coveralls.io/repos/github/ampleforth/ampleforth-contracts/badge.svg?branch=master)](https://coveralls.io/github/ampleforth/ampleforth-contracts?branch=master) 4 | 5 | Ampleforth (code name uFragments) is a decentralized elastic supply protocol. It maintains a stable unit price by adjusting supply directly to and from wallet holders. You can read the [whitepaper](https://www.ampleforth.org/paper/) for the motivation and a complete description of the protocol. 6 | 7 | This repository is a collection of [smart contracts](http://ampleforth.org/docs) that implement the Ampleforth protocol on the Ethereum blockchain. 8 | 9 | The official mainnet addresses are: 10 | 11 | - ERC-20 Token: [0xD46bA6D942050d489DBd938a2C909A5d5039A161](https://etherscan.io/token/0xd46ba6d942050d489dbd938a2c909a5d5039a161) 12 | - Supply Policy: [0x1B228a749077b8e307C5856cE62Ef35d96Dca2ea](https://etherscan.io/address/0x1b228a749077b8e307c5856ce62ef35d96dca2ea) 13 | - Orchestrator: [0x6fb00a180781e75f87e2b690af0196baa77c7e7c](https://etherscan.io/address/0x6fb00a180781e75f87e2b690af0196baa77c7e7c) 14 | - Market Oracle: [0x99c9775e076fdf99388c029550155032ba2d8914](https://etherscan.io/address/0x99c9775e076fdf99388c029550155032ba2d8914) 15 | - CPI Oracle: [0xa759f960dd59a1ad32c995ecabe802a0c35f244f](https://etherscan.io/address/0xa759f960dd59a1ad32c995ecabe802a0c35f244f) 16 | - WAMPL: [0xEDB171C18cE90B633DB442f2A6F72874093b49Ef](https://etherscan.io/address/0xEDB171C18cE90B633DB442f2A6F72874093b49Ef) 17 | 18 | ## Table of Contents 19 | 20 | - [Install](#install) 21 | - [Testing](#testing) 22 | - [Testnets](#testnets) 23 | - [Contribute](#contribute) 24 | - [License](#license) 25 | 26 | ## Install 27 | 28 | ```bash 29 | # Install project dependencies 30 | yarn 31 | ``` 32 | 33 | ## Testing 34 | 35 | ```bash 36 | # Run all unit tests (compatible with node v12+) 37 | yarn test 38 | ``` 39 | 40 | ## Testnets 41 | 42 | There is a testnet deployment on Goerli. It rebases hourly using real market data. 43 | 44 | - ERC-20 Token: [0x08c5b39F000705ebeC8427C1d64D6262392944EE](https://goerli.etherscan.io/token/0x08c5b39F000705ebeC8427C1d64D6262392944EE) 45 | - Supply Policy: [0x047b82a5D79d9DF62dE4f34CbaBa83F71848a6BF](https://goerli.etherscan.io/address/0x047b82a5D79d9DF62dE4f34CbaBa83F71848a6BF) 46 | - Orchestrator: [0x0ec93391752ef1A06AA2b83D15c3a5814651C891](https://goerli.etherscan.io/address/0x0ec93391752ef1A06AA2b83D15c3a5814651C891) 47 | - Market Oracle: [0xd4F96E4aC4B4f4E2359734a89b5484196298B69D](https://goerli.etherscan.io/address/0xd4F96E4aC4B4f4E2359734a89b5484196298B69D) 48 | - CPI Oracle: [0x53c75D13a07AA02615Cb43e942829862C963D9bf](https://goerli.etherscan.io/address/0x53c75D13a07AA02615Cb43e942829862C963D9bf) 49 | - Admin: [0x02C32fB5498e89a8750cc9Bd66382a681665c3a3](https://goerli.etherscan.io/address/0x02C32fB5498e89a8750cc9Bd66382a681665c3a3) 50 | - WAMPL: [0x3b624861a14979537DE1B88F9565F41a7fc45FBf](https://goerli.etherscan.io/address/0x3b624861a14979537DE1B88F9565F41a7fc45FBf) 51 | 52 | ## Contribute 53 | 54 | To report bugs within this package, create an issue in this repository. 55 | For security issues, please contact dev-support@ampleforth.org. 56 | When submitting code ensure that it is free of lint errors and has 100% test coverage. 57 | 58 | ```bash 59 | # Lint code 60 | yarn lint 61 | 62 | # Format code 63 | yarn format 64 | 65 | # Run solidity coverage report (compatible with node v12) 66 | yarn coverage 67 | 68 | # Run solidity gas usage report 69 | yarn profile 70 | ``` 71 | 72 | ## License 73 | 74 | [GNU General Public License v3.0 (c) 2018 Fragments, Inc.](./LICENSE) 75 | -------------------------------------------------------------------------------- /test/simulation/transfer_precision.ts: -------------------------------------------------------------------------------- 1 | /* 2 | In this script, we generate random cycles of fragments growth and contraction 3 | and test the precision of fragments transfers 4 | During every iteration; percentageGrowth is sampled from a unifrom distribution between [-50%,250%] 5 | and the fragments total supply grows/contracts. 6 | In each cycle we test the following guarantees: 7 | - If address 'A' transfers x fragments to address 'B'. A's resulting external balance will 8 | be decreased by precisely x fragments, and B's external balance will be precisely 9 | increased by x fragments. 10 | */ 11 | 12 | import { ethers, upgrades } from 'hardhat' 13 | import { expect } from 'chai' 14 | import { BigNumber, BigNumberish, Contract, Signer } from 'ethers' 15 | import { imul } from '../utils/utils' 16 | const Stochasm = require('stochasm') 17 | 18 | const endSupply = ethers.BigNumber.from(2).pow(128).sub(1) 19 | const uFragmentsGrowth = new Stochasm({ 20 | min: -0.5, 21 | max: 2.5, 22 | seed: 'fragments.org', 23 | }) 24 | 25 | let uFragments: Contract, 26 | inflation: BigNumber, 27 | rebaseAmt = ethers.BigNumber.from(0), 28 | preRebaseSupply = ethers.BigNumber.from(0), 29 | postRebaseSupply = ethers.BigNumber.from(0) 30 | 31 | async function checkBalancesAfterOperation( 32 | users: Signer[], 33 | op: Function, 34 | chk: Function, 35 | ) { 36 | const _bals = [] 37 | const bals = [] 38 | let u 39 | for (u in users) { 40 | if (Object.prototype.hasOwnProperty.call(users, u)) { 41 | _bals.push(await uFragments.balanceOf(users[u].getAddress())) 42 | } 43 | } 44 | await op() 45 | for (u in users) { 46 | if (Object.prototype.hasOwnProperty.call(users, u)) { 47 | bals.push(await uFragments.balanceOf(users[u].getAddress())) 48 | } 49 | } 50 | chk(_bals, bals) 51 | } 52 | 53 | async function checkBalancesAfterTransfer(users: Signer[], tAmt: BigNumberish) { 54 | await checkBalancesAfterOperation( 55 | users, 56 | async function () { 57 | await uFragments.connect(users[0]).transfer(users[1].getAddress(), tAmt) 58 | }, 59 | function ([_u0Bal, _u1Bal]: BigNumber[], [u0Bal, u1Bal]: BigNumber[]) { 60 | const _sum = _u0Bal.add(_u1Bal) 61 | const sum = u0Bal.add(u1Bal) 62 | expect(_sum).to.eq(sum) 63 | expect(_u0Bal.sub(tAmt)).to.eq(u0Bal) 64 | expect(_u1Bal.add(tAmt)).to.eq(u1Bal) 65 | }, 66 | ) 67 | } 68 | 69 | async function exec() { 70 | const [deployer, user] = await ethers.getSigners() 71 | const factory = await ethers.getContractFactory('UFragments') 72 | uFragments = await upgrades.deployProxy( 73 | factory.connect(deployer), 74 | [await deployer.getAddress()], 75 | { 76 | initializer: 'initialize(address)', 77 | }, 78 | ) 79 | await uFragments.connect(deployer).setMonetaryPolicy(deployer.getAddress()) 80 | 81 | let i = 0 82 | do { 83 | await uFragments.connect(deployer).rebase(i + 1, rebaseAmt) 84 | postRebaseSupply = await uFragments.totalSupply() 85 | i++ 86 | 87 | console.log('Rebased iteration', i) 88 | console.log('Rebased by', rebaseAmt.toString(), 'AMPL') 89 | console.log('Total supply is now', postRebaseSupply.toString(), 'AMPL') 90 | 91 | console.log('Testing precision of 1c transfer') 92 | await checkBalancesAfterTransfer([deployer, user], 1) 93 | await checkBalancesAfterTransfer([user, deployer], 1) 94 | 95 | console.log('Testing precision of max denomination') 96 | const tAmt = await uFragments.balanceOf(deployer.getAddress()) 97 | await checkBalancesAfterTransfer([deployer, user], tAmt) 98 | await checkBalancesAfterTransfer([user, deployer], tAmt) 99 | 100 | preRebaseSupply = await uFragments.totalSupply() 101 | inflation = uFragmentsGrowth.next().toFixed(5) 102 | rebaseAmt = imul(preRebaseSupply, inflation, 1) 103 | } while ((await uFragments.totalSupply()).add(rebaseAmt).lt(endSupply)) 104 | } 105 | 106 | describe('Transfer Precision', function () { 107 | it('should successfully run simulation', async function () { 108 | await exec() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /contracts/lib/SafeMathInt.sol: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2018 requestnetwork 5 | Copyright (c) 2018 Fragments, Inc. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | // SPDX-License-Identifier: GPL-3.0-or-later 27 | pragma solidity 0.8.4; 28 | 29 | /** 30 | * @title SafeMathInt 31 | * @dev Math operations for int256 with overflow safety checks. 32 | */ 33 | library SafeMathInt { 34 | int256 private constant MIN_INT256 = int256(1) << 255; 35 | int256 private constant MAX_INT256 = ~(int256(1) << 255); 36 | 37 | /** 38 | * @dev Multiplies two int256 variables and fails on overflow. 39 | */ 40 | function mul(int256 a, int256 b) internal pure returns (int256) { 41 | int256 c = a * b; 42 | 43 | // Detect overflow when multiplying MIN_INT256 with -1 44 | require(c != MIN_INT256 || (a & MIN_INT256) != (b & MIN_INT256)); 45 | require((b == 0) || (c / b == a)); 46 | return c; 47 | } 48 | 49 | /** 50 | * @dev Division of two int256 variables and fails on overflow. 51 | */ 52 | function div(int256 a, int256 b) internal pure returns (int256) { 53 | // Prevent overflow when dividing MIN_INT256 by -1 54 | require(b != -1 || a != MIN_INT256); 55 | 56 | // Solidity already throws when dividing by 0. 57 | return a / b; 58 | } 59 | 60 | /** 61 | * @dev Subtracts two int256 variables and fails on overflow. 62 | */ 63 | function sub(int256 a, int256 b) internal pure returns (int256) { 64 | int256 c = a - b; 65 | require((b >= 0 && c <= a) || (b < 0 && c > a)); 66 | return c; 67 | } 68 | 69 | /** 70 | * @dev Adds two int256 variables and fails on overflow. 71 | */ 72 | function add(int256 a, int256 b) internal pure returns (int256) { 73 | int256 c = a + b; 74 | require((b >= 0 && c >= a) || (b < 0 && c < a)); 75 | return c; 76 | } 77 | 78 | /** 79 | * @dev Converts to absolute value, and fails on overflow. 80 | */ 81 | function abs(int256 a) internal pure returns (int256) { 82 | require(a != MIN_INT256); 83 | return a < 0 ? -a : a; 84 | } 85 | 86 | /** 87 | * @dev Computes 2^exp with limited precision where -100 <= exp <= 100 * one 88 | * @param one 1.0 represented in the same fixed point number format as exp 89 | * @param exp The power to raise 2 to -100 <= exp <= 100 * one 90 | * @return 2^exp represented with same number of decimals after the point as one 91 | */ 92 | function twoPower(int256 exp, int256 one) internal pure returns (int256) { 93 | bool reciprocal = false; 94 | if (exp < 0) { 95 | reciprocal = true; 96 | exp = abs(exp); 97 | } 98 | 99 | // Precomputed values for 2^(1/2^i) in 18 decimals fixed point numbers 100 | int256[5] memory ks = [ 101 | int256(1414213562373095049), 102 | 1189207115002721067, 103 | 1090507732665257659, 104 | 1044273782427413840, 105 | 1021897148654116678 106 | ]; 107 | int256 whole = div(exp, one); 108 | require(whole <= 100); 109 | int256 result = mul(int256(uint256(1) << uint256(whole)), one); 110 | int256 remaining = sub(exp, mul(whole, one)); 111 | 112 | int256 current = div(one, 2); 113 | for (uint256 i = 0; i < 5; i++) { 114 | if (remaining >= current) { 115 | remaining = sub(remaining, current); 116 | result = div(mul(result, ks[i]), 10**18); // 10**18 to match hardcoded ks values 117 | } 118 | current = div(current, 2); 119 | } 120 | if (reciprocal) { 121 | result = div(mul(one, one), result); 122 | } 123 | return result; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/unit/UFragments_erc20_permit.ts: -------------------------------------------------------------------------------- 1 | import { network, ethers, upgrades } from 'hardhat' 2 | import { Contract, Signer, Wallet, BigNumber } from 'ethers' 3 | import { expect } from 'chai' 4 | 5 | import { 6 | EIP712_DOMAIN_TYPEHASH, 7 | EIP2612_PERMIT_TYPEHASH, 8 | getDomainSeparator, 9 | signEIP712Permission, 10 | } from '../utils/signatures' 11 | 12 | let accounts: Signer[], 13 | deployer: Signer, 14 | deployerAddress: string, 15 | owner: Wallet, 16 | ownerAddress: string, 17 | spender: Wallet, 18 | spenderAddress: string, 19 | uFragments: Contract, 20 | initialSupply: BigNumber 21 | 22 | async function setupContracts() { 23 | // prepare signers 24 | accounts = await ethers.getSigners() 25 | deployer = accounts[0] 26 | deployerAddress = await deployer.getAddress() 27 | 28 | owner = Wallet.createRandom() 29 | ownerAddress = await owner.getAddress() 30 | 31 | spender = Wallet.createRandom() 32 | spenderAddress = await spender.getAddress() 33 | 34 | // deploy upgradable token 35 | const factory = await ethers.getContractFactory('UFragments') 36 | uFragments = await upgrades.deployProxy(factory, [deployerAddress], { 37 | initializer: 'initialize(address)', 38 | }) 39 | // fetch initial supply 40 | initialSupply = await uFragments.totalSupply() 41 | } 42 | 43 | // https://eips.ethereum.org/EIPS/eip-2612 44 | // Test cases as in: 45 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js 46 | describe('UFragments:Initialization', () => { 47 | before('setup UFragments contract', setupContracts) 48 | 49 | it('should set the EIP2612 parameters', async function () { 50 | expect(await uFragments.EIP712_REVISION()).to.eq('1') 51 | expect(await uFragments.EIP712_DOMAIN()).to.eq(EIP712_DOMAIN_TYPEHASH) 52 | expect(await uFragments.PERMIT_TYPEHASH()).to.eq(EIP2612_PERMIT_TYPEHASH) 53 | // with hard-coded parameters 54 | expect(await uFragments.DOMAIN_SEPARATOR()).to.eq( 55 | getDomainSeparator( 56 | await uFragments.EIP712_REVISION(), 57 | await uFragments.name(), 58 | uFragments.address, 59 | network.config.chainId || 1, 60 | ), 61 | ) 62 | }) 63 | 64 | it('initial nonce is 0', async function () { 65 | expect(await uFragments.nonces(deployerAddress)).to.eq('0') 66 | expect(await uFragments.nonces(ownerAddress)).to.eq('0') 67 | expect(await uFragments.nonces(spenderAddress)).to.eq('0') 68 | }) 69 | }) 70 | 71 | // Using the cases specified by: 72 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js 73 | describe('UFragments:EIP-2612 Permit', () => { 74 | const MAX_DEADLINE = BigNumber.from(2).pow(256).sub(1) 75 | 76 | beforeEach('setup UFragments contract', setupContracts) 77 | 78 | describe('permit', function () { 79 | const signPermission = async ( 80 | signer: Wallet, 81 | owner: string, 82 | spender: string, 83 | value: number, 84 | nonce: number, 85 | deadline: BigNumber, 86 | ) => { 87 | return signEIP712Permission( 88 | await uFragments.EIP712_REVISION(), 89 | await uFragments.name(), 90 | uFragments.address, 91 | network.config.chainId || 1, 92 | signer, 93 | owner, 94 | spender, 95 | value, 96 | nonce, 97 | deadline, 98 | ) 99 | } 100 | 101 | it('accepts owner signature', async function () { 102 | const { v, r, s } = await signPermission( 103 | owner, 104 | ownerAddress, 105 | spenderAddress, 106 | 123, 107 | 0, 108 | MAX_DEADLINE, 109 | ) 110 | await expect( 111 | uFragments 112 | .connect(deployer) 113 | .permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s), 114 | ) 115 | .to.emit(uFragments, 'Approval') 116 | .withArgs(ownerAddress, spenderAddress, '123') 117 | expect(await uFragments.nonces(ownerAddress)).to.eq('1') 118 | expect(await uFragments.allowance(ownerAddress, spenderAddress)).to.eq( 119 | '123', 120 | ) 121 | }) 122 | 123 | it('rejects reused signature', async function () { 124 | const { v, r, s } = await signPermission( 125 | owner, 126 | ownerAddress, 127 | spenderAddress, 128 | 123, 129 | 0, 130 | MAX_DEADLINE, 131 | ) 132 | await uFragments 133 | .connect(deployer) 134 | .permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s) 135 | await expect( 136 | uFragments 137 | .connect(deployer) 138 | .permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s), 139 | ).to.be.reverted 140 | }) 141 | 142 | it('rejects other signature', async function () { 143 | const { v, r, s } = await signPermission( 144 | spender, 145 | ownerAddress, 146 | spenderAddress, 147 | 123, 148 | 0, 149 | MAX_DEADLINE, 150 | ) 151 | await expect( 152 | uFragments 153 | .connect(deployer) 154 | .permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s), 155 | ).to.be.reverted 156 | }) 157 | 158 | it('rejects expired permit', async function () { 159 | const currentTs = (await ethers.provider.getBlock('latest')).timestamp 160 | const olderTs = currentTs - 3600 * 24 * 7 161 | const deadline = BigNumber.from(olderTs) 162 | const { v, r, s } = await signPermission( 163 | owner, 164 | ownerAddress, 165 | spenderAddress, 166 | 123, 167 | 0, 168 | deadline, 169 | ) 170 | await expect( 171 | uFragments 172 | .connect(deployer) 173 | .permit(ownerAddress, spenderAddress, 123, deadline, v, r, s), 174 | ).to.be.reverted 175 | }) 176 | }) 177 | }) 178 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, utils, constants } from 'ethers' 2 | import { task } from 'hardhat/config' 3 | import { Interface } from '@ethersproject/abi' 4 | import { getImplementationAddress } from '@openzeppelin/upgrades-core' 5 | 6 | import { 7 | waitFor, 8 | deployContract, 9 | deployProxy, 10 | deployExternalArtifact, 11 | verify, 12 | } from './helpers' 13 | 14 | task('deploy:amplforce:testnet', 'Deploy ampleforth contract suite for testnet') 15 | .addFlag('verify', 'The ERC-20 name of the wAMPL token') 16 | .addFlag('setupMultisig', 'Sets up multisig admin and transfers onwership') 17 | .setAction(async (args, hre) => { 18 | // HARD-CODED config values 19 | 20 | // The value of CPI which was set when v1.0.0 contracts were deployed in july 2019 21 | const INITIAL_CPI = BigNumber.from('109195000000000007392') 22 | const INITIAL_RATE = utils.parseUnits('1', 18) // 1.0 23 | 24 | // Rate oracle 25 | const RATE_REPORT_EXPIRATION_SEC = 86400 // 1 day 26 | const RATE_REPORT_DELAY_SEC = 0 27 | const RATE_MIN_PROVIDERS = 1 28 | 29 | // CPI oracle 30 | const CPI_REPORT_EXPIRATION_SEC = 7776000 // 90 days 31 | const CPI_REPORT_DELAY_SEC = 0 32 | const CPI_MIN_PROVIDERS = 1 33 | 34 | // Policy 35 | const DEVIATION_TRESHOLD = utils.parseUnits('0.002', 18) // 0.002% (ie) 0.05/24) 36 | const LOWER = utils.parseUnits('-0.005', 18) 37 | const UPPER = utils.parseUnits('0.005', 18) 38 | const GROWTH = utils.parseUnits('3', 18) 39 | const MIN_REBASE_INTERVAL = 1200 // 20 mins 40 | const REBASE_WINDOW_OFFSET = 0 41 | const REBASE_WINDOW_LEN = 2400 // 40 mins 42 | 43 | // AMPL 44 | const DECIMALS = 9 45 | 46 | // get signers 47 | const deployer = (await hre.ethers.getSigners())[0] 48 | const owner = await deployer.getAddress() 49 | console.log('Deployer', owner) 50 | 51 | // deploy ampl erc-20 52 | const ampl = await deployProxy( 53 | hre, 54 | 'UFragments', 55 | deployer, 56 | 'initialize(address)', 57 | [owner], 58 | ) 59 | const amplImpl = await getImplementationAddress( 60 | hre.ethers.provider, 61 | ampl.address, 62 | ) 63 | console.log('UFragments deployed to:', ampl.address) 64 | console.log('Implementation:', amplImpl) 65 | 66 | // deploy market oracle 67 | const marketOracle = await deployContract( 68 | hre, 69 | 'MedianOracle', 70 | deployer, 71 | ) 72 | await marketOracle.init( 73 | RATE_REPORT_EXPIRATION_SEC, 74 | RATE_REPORT_DELAY_SEC, 75 | RATE_MIN_PROVIDERS, 76 | ) 77 | console.log('Market oracle to:', marketOracle.address) 78 | 79 | // deploy cpi oracle 80 | const cpiOracle = await deployContract( 81 | hre, 82 | 'MedianOracle', 83 | deployer, 84 | ) 85 | await cpiOracle.init( 86 | CPI_REPORT_EXPIRATION_SEC, 87 | CPI_REPORT_DELAY_SEC, 88 | CPI_MIN_PROVIDERS, 89 | ) 90 | console.log('CPI oracle to:', cpiOracle.address) 91 | 92 | // deploy policy 93 | const policy = await deployProxy( 94 | hre, 95 | 'UFragmentsPolicy', 96 | deployer, 97 | 'initialize(address,address,uint256)', 98 | [owner, ampl.address, INITIAL_CPI.toString()], 99 | ) 100 | const policyImpl = await getImplementationAddress( 101 | hre.ethers.provider, 102 | policy.address, 103 | ) 104 | console.log('UFragmentsPolicy deployed to:', policy.address) 105 | console.log('Implementation:', policyImpl) 106 | 107 | // deploy orchestrator 108 | const orchestratorParams = [policy.address] 109 | const orchestrator = await deployContract( 110 | hre, 111 | 'Orchestrator', 112 | deployer, 113 | orchestratorParams, 114 | ) 115 | console.log('Orchestrator deployed to:', orchestrator.address) 116 | 117 | // Set references 118 | await waitFor(ampl.connect(deployer).setMonetaryPolicy(policy.address)) 119 | await waitFor( 120 | policy.connect(deployer).setMarketOracle(marketOracle.address), 121 | ) 122 | await waitFor(policy.connect(deployer).setCpiOracle(cpiOracle.address)) 123 | await waitFor( 124 | policy.connect(deployer).setOrchestrator(orchestrator.address), 125 | ) 126 | console.log('References set') 127 | 128 | // configure parameters 129 | await waitFor(policy.setDeviationThreshold(DEVIATION_TRESHOLD)) 130 | await waitFor(policy.setRebaseFunctionGrowth(GROWTH)) 131 | await waitFor(policy.setRebaseFunctionLowerPercentage(LOWER)) 132 | await waitFor(policy.setRebaseFunctionUpperPercentage(UPPER)) 133 | await waitFor( 134 | policy.setRebaseTimingParameters( 135 | MIN_REBASE_INTERVAL, 136 | REBASE_WINDOW_OFFSET, 137 | REBASE_WINDOW_LEN, 138 | ), 139 | ) 140 | await waitFor(marketOracle.addProvider(owner)) 141 | await waitFor(cpiOracle.addProvider(owner)) 142 | console.log('Parameters configured') 143 | 144 | // initial rebase 145 | await waitFor(marketOracle.pushReport(INITIAL_RATE)) 146 | await waitFor(cpiOracle.pushReport(INITIAL_CPI)) 147 | await waitFor(orchestrator.rebase()) 148 | const r = await policy.globalAmpleforthEpochAndAMPLSupply() 149 | console.log( 150 | `Rebase success: ${r[0].toString()} ${utils.formatUnits( 151 | r[1].toString(), 152 | DECIMALS, 153 | )}`, 154 | ) 155 | 156 | // transferring ownership to multisig 157 | if (args.setupMultisig) { 158 | const adminWallet = await deployExternalArtifact( 159 | hre, 160 | 'MultiSigWallet', 161 | deployer, 162 | [[owner], 1], 163 | ) 164 | console.log('Admin/Provider wallet: ', adminWallet.address) 165 | await waitFor(marketOracle.addProvider(adminWallet.address)) 166 | await waitFor(cpiOracle.addProvider(adminWallet.address)) 167 | 168 | console.log('Transferring ownership to: ', adminWallet.address) 169 | await waitFor(marketOracle.transferOwnership(adminWallet.address)) 170 | await waitFor(cpiOracle.transferOwnership(adminWallet.address)) 171 | await waitFor(ampl.transferOwnership(adminWallet.address)) 172 | await waitFor(policy.transferOwnership(adminWallet.address)) 173 | await waitFor(orchestrator.transferOwnership(adminWallet.address)) 174 | } 175 | 176 | // verification 177 | if (args.verify) { 178 | console.log('Verifying contracts:') 179 | await verify(hre, marketOracle.address) 180 | await verify(hre, cpiOracle.address) 181 | await verify(hre, orchestrator.address, orchestratorParams) 182 | await verify(hre, ampl.address) 183 | await verify(hre, policy.address) 184 | await verify(hre, amplImpl) 185 | await verify(hre, policyImpl) 186 | } 187 | }) 188 | 189 | task('deploy:wampl', 'Deploy wampl contract') 190 | .addParam('ampl', 'The address to the AMPL token') 191 | .addParam('name', 'The ERC-20 name of the wAMPL token') 192 | .addParam('symbol', 'The ERC-20 symbol of the wAMPL token') 193 | .setAction(async (args, hre) => { 194 | console.log(args) 195 | 196 | // get signers 197 | const deployer = (await hre.ethers.getSigners())[0] 198 | console.log('Deployer', await deployer.getAddress()) 199 | 200 | // deploy contract 201 | const wampl = await deployContract(hre, 'WAMPL', deployer, [args.ampl]) 202 | await wampl.init(args.name, args.symbol) 203 | console.log('wAMPL deployed to:', wampl.address) 204 | 205 | // wait and verify 206 | await wampl.deployTransaction.wait(5) 207 | await verify(hre, wampl.address, [args.ampl]) 208 | }) 209 | -------------------------------------------------------------------------------- /test/unit/uFragments_elastic_behavior.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades, waffle } from 'hardhat' 2 | import { Contract, Signer, BigNumber } from 'ethers' 3 | import { TransactionResponse } from '@ethersproject/providers' 4 | import { expect } from 'chai' 5 | 6 | const DECIMALS = 9 7 | const INITIAL_SUPPLY = ethers.utils.parseUnits('50', 6 + DECIMALS) 8 | const MAX_UINT256 = ethers.BigNumber.from(2).pow(256).sub(1) 9 | const MAX_INT256 = ethers.BigNumber.from(2).pow(255).sub(1) 10 | const TOTAL_GONS = MAX_UINT256.sub(MAX_UINT256.mod(INITIAL_SUPPLY)) 11 | 12 | const toUFrgDenomination = (ample: string): BigNumber => 13 | ethers.utils.parseUnits(ample, DECIMALS) 14 | 15 | const unitTokenAmount = toUFrgDenomination('1') 16 | 17 | let token: Contract, owner: Signer, anotherAccount: Signer, recipient: Signer 18 | 19 | async function upgradeableToken() { 20 | const [owner, recipient, anotherAccount] = await ethers.getSigners() 21 | const factory = await ethers.getContractFactory('UFragments') 22 | const token = await upgrades.deployProxy( 23 | factory.connect(owner), 24 | [await owner.getAddress()], 25 | { 26 | initializer: 'initialize(address)', 27 | }, 28 | ) 29 | return { token, owner, recipient, anotherAccount } 30 | } 31 | 32 | describe('UFragments:Elastic', () => { 33 | beforeEach('setup UFragments contract', async function () { 34 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 35 | upgradeableToken, 36 | )) 37 | }) 38 | 39 | describe('scaledTotalSupply', function () { 40 | it('returns the scaled total amount of tokens', async function () { 41 | expect(await token.scaledTotalSupply()).to.eq(TOTAL_GONS) 42 | }) 43 | }) 44 | 45 | describe('scaledBalanceOf', function () { 46 | describe('when the requested account has no tokens', function () { 47 | it('returns zero', async function () { 48 | expect( 49 | await token.scaledBalanceOf(await anotherAccount.getAddress()), 50 | ).to.eq(0) 51 | }) 52 | }) 53 | 54 | describe('when the requested account has some tokens', function () { 55 | it('returns the total amount of tokens', async function () { 56 | expect(await token.scaledBalanceOf(await owner.getAddress())).to.eq( 57 | TOTAL_GONS, 58 | ) 59 | }) 60 | }) 61 | }) 62 | }) 63 | 64 | describe('UFragments:Elastic:transferAll', () => { 65 | beforeEach('setup UFragments contract', async function () { 66 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 67 | upgradeableToken, 68 | )) 69 | }) 70 | 71 | describe('when the recipient is the zero address', function () { 72 | it('should revert', async function () { 73 | await expect( 74 | token.connect(owner).transferAll(ethers.constants.AddressZero), 75 | ).to.be.reverted 76 | }) 77 | }) 78 | 79 | describe('when the recipient is the contract address', function () { 80 | it('should revert', async function () { 81 | await expect(token.connect(owner).transferAll(token.address)).to.be 82 | .reverted 83 | }) 84 | }) 85 | 86 | describe('when the sender has zero balance', function () { 87 | it('should not revert', async function () { 88 | await expect( 89 | token.connect(anotherAccount).transferAll(await owner.getAddress()), 90 | ).not.to.be.reverted 91 | }) 92 | }) 93 | 94 | describe('when the sender has balance', function () { 95 | it('should emit a transfer event', async function () { 96 | await expect( 97 | token.connect(owner).transferAll(await recipient.getAddress()), 98 | ) 99 | .to.emit(token, 'Transfer') 100 | .withArgs( 101 | await owner.getAddress(), 102 | await recipient.getAddress(), 103 | INITIAL_SUPPLY, 104 | ) 105 | }) 106 | 107 | it("should transfer all of the sender's balance", async function () { 108 | const senderBalance = await token.balanceOf(await owner.getAddress()) 109 | const recipientBalance = await token.balanceOf( 110 | await recipient.getAddress(), 111 | ) 112 | await token.connect(owner).transferAll(await recipient.getAddress()) 113 | const senderBalance_ = await token.balanceOf(await owner.getAddress()) 114 | const recipientBalance_ = await token.balanceOf( 115 | await recipient.getAddress(), 116 | ) 117 | expect(senderBalance_).to.eq('0') 118 | expect(recipientBalance_.sub(recipientBalance)).to.eq(senderBalance) 119 | }) 120 | }) 121 | }) 122 | 123 | describe('UFragments:Elastic:transferAllFrom', () => { 124 | beforeEach('setup UFragments contract', async function () { 125 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 126 | upgradeableToken, 127 | )) 128 | }) 129 | 130 | describe('when the recipient is the zero address', function () { 131 | it('should revert', async function () { 132 | const senderBalance = await token.balanceOf(await owner.getAddress()) 133 | await token 134 | .connect(owner) 135 | .approve(await anotherAccount.getAddress(), senderBalance) 136 | await expect( 137 | token 138 | .connect(anotherAccount) 139 | .transferAllFrom( 140 | await owner.getAddress(), 141 | ethers.constants.AddressZero, 142 | ), 143 | ).to.be.reverted 144 | }) 145 | }) 146 | 147 | describe('when the recipient is the contract address', function () { 148 | it('should revert', async function () { 149 | const senderBalance = await token.balanceOf(await owner.getAddress()) 150 | await token 151 | .connect(owner) 152 | .approve(await anotherAccount.getAddress(), senderBalance) 153 | await expect( 154 | token 155 | .connect(anotherAccount) 156 | .transferAllFrom(await owner.getAddress(), token.address), 157 | ).to.be.reverted 158 | }) 159 | }) 160 | 161 | describe('when the sender has zero balance', function () { 162 | it('should not revert', async function () { 163 | const senderBalance = await token.balanceOf( 164 | await anotherAccount.getAddress(), 165 | ) 166 | await token 167 | .connect(anotherAccount) 168 | .approve(await anotherAccount.getAddress(), senderBalance) 169 | 170 | await expect( 171 | token 172 | .connect(recipient) 173 | .transferAllFrom( 174 | await anotherAccount.getAddress(), 175 | await recipient.getAddress(), 176 | ), 177 | ).not.to.be.reverted 178 | }) 179 | }) 180 | 181 | describe('when the spender does NOT have enough approved balance', function () { 182 | it('reverts', async function () { 183 | await token 184 | .connect(owner) 185 | .approve(await anotherAccount.getAddress(), unitTokenAmount) 186 | await expect( 187 | token 188 | .connect(anotherAccount) 189 | .transferAllFrom( 190 | await owner.getAddress(), 191 | await recipient.getAddress(), 192 | ), 193 | ).to.be.reverted 194 | }) 195 | }) 196 | 197 | describe('when the spender has enough approved balance', function () { 198 | it('emits a transfer event', async function () { 199 | const senderBalance = await token.balanceOf(await owner.getAddress()) 200 | await token 201 | .connect(owner) 202 | .approve(await anotherAccount.getAddress(), senderBalance) 203 | 204 | await expect( 205 | token 206 | .connect(anotherAccount) 207 | .transferAllFrom( 208 | await owner.getAddress(), 209 | await recipient.getAddress(), 210 | ), 211 | ) 212 | .to.emit(token, 'Transfer') 213 | .withArgs( 214 | await owner.getAddress(), 215 | await recipient.getAddress(), 216 | senderBalance, 217 | ) 218 | }) 219 | 220 | it('transfers the requested amount', async function () { 221 | const senderBalance = await token.balanceOf(await owner.getAddress()) 222 | const recipientBalance = await token.balanceOf( 223 | await recipient.getAddress(), 224 | ) 225 | 226 | await token 227 | .connect(owner) 228 | .approve(await anotherAccount.getAddress(), senderBalance) 229 | 230 | await token 231 | .connect(anotherAccount) 232 | .transferAllFrom(await owner.getAddress(), await recipient.getAddress()) 233 | 234 | const senderBalance_ = await token.balanceOf(await owner.getAddress()) 235 | const recipientBalance_ = await token.balanceOf( 236 | await recipient.getAddress(), 237 | ) 238 | expect(senderBalance_).to.eq('0') 239 | expect(recipientBalance_.sub(recipientBalance)).to.eq(senderBalance) 240 | }) 241 | 242 | it('decreases the spender allowance', async function () { 243 | const senderBalance = await token.balanceOf(await owner.getAddress()) 244 | await token 245 | .connect(owner) 246 | .approve(await anotherAccount.getAddress(), senderBalance.add('99')) 247 | await token 248 | .connect(anotherAccount) 249 | .transferAllFrom(await owner.getAddress(), await recipient.getAddress()) 250 | expect( 251 | await token.allowance( 252 | await owner.getAddress(), 253 | await anotherAccount.getAddress(), 254 | ), 255 | ).to.eq('99') 256 | }) 257 | }) 258 | }) 259 | -------------------------------------------------------------------------------- /contracts/MedianOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import "./lib/Select.sol"; 6 | 7 | interface IOracle { 8 | function getData() external returns (uint256, bool); 9 | } 10 | 11 | /** 12 | * @title Median Oracle 13 | * 14 | * @notice Provides a value onchain that's aggregated from a whitelisted set of 15 | * providers. 16 | */ 17 | contract MedianOracle is OwnableUpgradeable, IOracle { 18 | struct Report { 19 | uint256 timestamp; 20 | uint256 payload; 21 | } 22 | 23 | // Addresses of providers authorized to push reports. 24 | address[] public providers; 25 | 26 | // Reports indexed by provider address. Report[0].timestamp > 0 27 | // indicates provider existence. 28 | mapping(address => Report[2]) public providerReports; 29 | 30 | event ProviderAdded(address provider); 31 | event ProviderRemoved(address provider); 32 | event ReportTimestampOutOfRange(address provider); 33 | event ProviderReportPushed(address indexed provider, uint256 payload, uint256 timestamp); 34 | 35 | // The number of seconds after which the report is deemed expired. 36 | uint256 public reportExpirationTimeSec; 37 | 38 | // The number of seconds since reporting that has to pass before a report 39 | // is usable. 40 | uint256 public reportDelaySec; 41 | 42 | // The minimum number of providers with valid reports to consider the 43 | // aggregate report valid. 44 | uint256 public minimumProviders = 1; 45 | 46 | // Timestamp of 1 is used to mark uninitialized and invalidated data. 47 | // This is needed so that timestamp of 1 is always considered expired. 48 | uint256 private constant MAX_REPORT_EXPIRATION_TIME = 520 weeks; 49 | 50 | /** 51 | * @notice Contract state initialization. 52 | * 53 | * @param reportExpirationTimeSec_ The number of seconds after which the 54 | * report is deemed expired. 55 | * @param reportDelaySec_ The number of seconds since reporting that has to 56 | * pass before a report is usable 57 | * @param minimumProviders_ The minimum number of providers with valid 58 | * reports to consider the aggregate report valid. 59 | */ 60 | function init( 61 | uint256 reportExpirationTimeSec_, 62 | uint256 reportDelaySec_, 63 | uint256 minimumProviders_ 64 | ) public initializer { 65 | require(reportExpirationTimeSec_ <= MAX_REPORT_EXPIRATION_TIME); 66 | require(minimumProviders_ > 0); 67 | reportExpirationTimeSec = reportExpirationTimeSec_; 68 | reportDelaySec = reportDelaySec_; 69 | minimumProviders = minimumProviders_; 70 | __Ownable_init(); 71 | } 72 | 73 | /** 74 | * @notice Sets the report expiration period. 75 | * @param reportExpirationTimeSec_ The number of seconds after which the 76 | * report is deemed expired. 77 | */ 78 | function setReportExpirationTimeSec(uint256 reportExpirationTimeSec_) external onlyOwner { 79 | require(reportExpirationTimeSec_ <= MAX_REPORT_EXPIRATION_TIME); 80 | reportExpirationTimeSec = reportExpirationTimeSec_; 81 | } 82 | 83 | /** 84 | * @notice Sets the time period since reporting that has to pass before a 85 | * report is usable. 86 | * @param reportDelaySec_ The new delay period in seconds. 87 | */ 88 | function setReportDelaySec(uint256 reportDelaySec_) external onlyOwner { 89 | reportDelaySec = reportDelaySec_; 90 | } 91 | 92 | /** 93 | * @notice Sets the minimum number of providers with valid reports to 94 | * consider the aggregate report valid. 95 | * @param minimumProviders_ The new minimum number of providers. 96 | */ 97 | function setMinimumProviders(uint256 minimumProviders_) external onlyOwner { 98 | require(minimumProviders_ > 0); 99 | minimumProviders = minimumProviders_; 100 | } 101 | 102 | /** 103 | * @notice Pushes a report for the calling provider. 104 | * @param payload is expected to be 18 decimal fixed point number. 105 | */ 106 | function pushReport(uint256 payload) external { 107 | address providerAddress = msg.sender; 108 | Report[2] storage reports = providerReports[providerAddress]; 109 | uint256[2] memory timestamps = [reports[0].timestamp, reports[1].timestamp]; 110 | 111 | require(timestamps[0] > 0); 112 | 113 | uint8 index_recent = timestamps[0] >= timestamps[1] ? 0 : 1; 114 | uint8 index_past = 1 - index_recent; 115 | 116 | // Check that the push is not too soon after the last one. 117 | require(timestamps[index_recent] + reportDelaySec <= block.timestamp); 118 | 119 | reports[index_past].timestamp = block.timestamp; 120 | reports[index_past].payload = payload; 121 | 122 | emit ProviderReportPushed(providerAddress, payload, block.timestamp); 123 | } 124 | 125 | /** 126 | * @notice Invalidates the reports of the calling provider. 127 | */ 128 | function purgeReports() external { 129 | address providerAddress = msg.sender; 130 | require(providerReports[providerAddress][0].timestamp > 0); 131 | providerReports[providerAddress][0].timestamp = 1; 132 | providerReports[providerAddress][1].timestamp = 1; 133 | } 134 | 135 | /** 136 | * @notice Computes median of provider reports whose timestamps are in the 137 | * valid timestamp range. 138 | * @return AggregatedValue: Median of providers reported values. 139 | * valid: Boolean indicating an aggregated value was computed successfully. 140 | */ 141 | function getData() external override returns (uint256, bool) { 142 | uint256 reportsCount = providers.length; 143 | uint256[] memory validReports = new uint256[](reportsCount); 144 | uint256 size = 0; 145 | uint256 minValidTimestamp = block.timestamp - reportExpirationTimeSec; 146 | uint256 maxValidTimestamp = block.timestamp - reportDelaySec; 147 | 148 | for (uint256 i = 0; i < reportsCount; i++) { 149 | address providerAddress = providers[i]; 150 | Report[2] memory reports = providerReports[providerAddress]; 151 | 152 | uint8 index_recent = reports[0].timestamp >= reports[1].timestamp ? 0 : 1; 153 | uint8 index_past = 1 - index_recent; 154 | uint256 reportTimestampRecent = reports[index_recent].timestamp; 155 | if (reportTimestampRecent > maxValidTimestamp) { 156 | // Recent report is too recent. 157 | uint256 reportTimestampPast = providerReports[providerAddress][index_past] 158 | .timestamp; 159 | if (reportTimestampPast < minValidTimestamp) { 160 | // Past report is too old. 161 | emit ReportTimestampOutOfRange(providerAddress); 162 | } else if (reportTimestampPast > maxValidTimestamp) { 163 | // Past report is too recent. 164 | emit ReportTimestampOutOfRange(providerAddress); 165 | } else { 166 | // Using past report. 167 | validReports[size++] = providerReports[providerAddress][index_past].payload; 168 | } 169 | } else { 170 | // Recent report is not too recent. 171 | if (reportTimestampRecent < minValidTimestamp) { 172 | // Recent report is too old. 173 | emit ReportTimestampOutOfRange(providerAddress); 174 | } else { 175 | // Using recent report. 176 | validReports[size++] = providerReports[providerAddress][index_recent].payload; 177 | } 178 | } 179 | } 180 | 181 | if (size < minimumProviders) { 182 | return (0, false); 183 | } 184 | 185 | return (Select.computeMedian(validReports, size), true); 186 | } 187 | 188 | /** 189 | * @notice Authorizes a provider. 190 | * @param provider Address of the provider. 191 | */ 192 | function addProvider(address provider) external onlyOwner { 193 | require(providerReports[provider][0].timestamp == 0); 194 | providers.push(provider); 195 | providerReports[provider][0].timestamp = 1; 196 | emit ProviderAdded(provider); 197 | } 198 | 199 | /** 200 | * @notice Revokes provider authorization. 201 | * @param provider Address of the provider. 202 | */ 203 | function removeProvider(address provider) external onlyOwner { 204 | delete providerReports[provider]; 205 | for (uint256 i = 0; i < providers.length; i++) { 206 | if (providers[i] == provider) { 207 | if (i + 1 != providers.length) { 208 | providers[i] = providers[providers.length - 1]; 209 | } 210 | providers.pop(); 211 | emit ProviderRemoved(provider); 212 | break; 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * @return The number of authorized providers. 219 | */ 220 | function providersSize() external view returns (uint256) { 221 | return providers.length; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /test/unit/SafeMathInt.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { Contract } from 'ethers' 3 | import { expect } from 'chai' 4 | 5 | describe('SafeMathInt', () => { 6 | const MIN_INT256 = ethers.BigNumber.from(-2).pow(255) 7 | const MAX_INT256 = ethers.BigNumber.from(2).pow(255).sub(1) 8 | 9 | let safeMathInt: Contract 10 | 11 | beforeEach(async function () { 12 | // deploy contract 13 | const factory = await ethers.getContractFactory('SafeMathIntMock') 14 | safeMathInt = await factory.deploy() 15 | await safeMathInt.deployed() 16 | }) 17 | 18 | describe('add', function () { 19 | it('adds correctly', async function () { 20 | const a = ethers.BigNumber.from(5678) 21 | const b = ethers.BigNumber.from(1234) 22 | 23 | await expect(safeMathInt.add(a, b)) 24 | .to.emit(safeMathInt, 'ReturnValueInt256') 25 | .withArgs(a.add(b)) 26 | }) 27 | 28 | it('should fail on addition overflow', async function () { 29 | const a = MAX_INT256 30 | const b = ethers.BigNumber.from(1) 31 | 32 | await expect(safeMathInt.add(a, b)).to.be.reverted 33 | await expect(safeMathInt.add(b, a)).to.be.reverted 34 | }) 35 | 36 | it('should fail on addition overflow, swapped args', async function () { 37 | const a = ethers.BigNumber.from(1) 38 | const b = MAX_INT256 39 | 40 | await expect(safeMathInt.add(a, b)).to.be.reverted 41 | await expect(safeMathInt.add(b, a)).to.be.reverted 42 | }) 43 | 44 | it('should fail on addition negative overflow', async function () { 45 | const a = MIN_INT256 46 | const b = ethers.BigNumber.from(-1) 47 | 48 | await expect(safeMathInt.add(a, b)).to.be.reverted 49 | await expect(safeMathInt.add(b, a)).to.be.reverted 50 | }) 51 | }) 52 | 53 | describe('sub', function () { 54 | it('subtracts correctly', async function () { 55 | const a = ethers.BigNumber.from(5678) 56 | const b = ethers.BigNumber.from(1234) 57 | 58 | await expect(safeMathInt.sub(a, b)) 59 | .to.emit(safeMathInt, 'ReturnValueInt256') 60 | .withArgs(a.sub(b)) 61 | }) 62 | 63 | it('should fail on subtraction overflow', async function () { 64 | const a = MAX_INT256 65 | const b = ethers.BigNumber.from(-1) 66 | 67 | await expect(safeMathInt.sub(a, b)).to.be.reverted 68 | }) 69 | 70 | it('should fail on subtraction negative overflow', async function () { 71 | const a = MIN_INT256 72 | const b = ethers.BigNumber.from(1) 73 | 74 | await expect(safeMathInt.sub(a, b)).to.be.reverted 75 | }) 76 | }) 77 | 78 | describe('mul', function () { 79 | it('multiplies correctly', async function () { 80 | const a = ethers.BigNumber.from(1234) 81 | const b = ethers.BigNumber.from(5678) 82 | 83 | await expect(safeMathInt.mul(a, b)) 84 | .to.emit(safeMathInt, 'ReturnValueInt256') 85 | .withArgs(a.mul(b)) 86 | }) 87 | 88 | it('handles a zero product correctly', async function () { 89 | const a = ethers.BigNumber.from(0) 90 | const b = ethers.BigNumber.from(5678) 91 | 92 | await expect(safeMathInt.mul(a, b)) 93 | .to.emit(safeMathInt, 'ReturnValueInt256') 94 | .withArgs(a.mul(b)) 95 | }) 96 | 97 | it('should fail on multiplication overflow', async function () { 98 | const a = MAX_INT256 99 | const b = ethers.BigNumber.from(2) 100 | 101 | await expect(safeMathInt.mul(a, b)).to.be.reverted 102 | await expect(safeMathInt.mul(b, a)).to.be.reverted 103 | }) 104 | 105 | it('should fail on multiplication negative overflow', async function () { 106 | const a = MIN_INT256 107 | const b = ethers.BigNumber.from(2) 108 | 109 | await expect(safeMathInt.mul(a, b)).to.be.reverted 110 | await expect(safeMathInt.mul(b, a)).to.be.reverted 111 | }) 112 | 113 | it('should fail on multiplication between -1 and MIN_INT256', async function () { 114 | const a = MIN_INT256 115 | const b = ethers.BigNumber.from(-1) 116 | 117 | await expect(safeMathInt.mul(a, b)).to.be.reverted 118 | await expect(safeMathInt.mul(b, a)).to.be.reverted 119 | }) 120 | }) 121 | 122 | describe('div', function () { 123 | it('divides correctly', async function () { 124 | const a = ethers.BigNumber.from(5678) 125 | const b = ethers.BigNumber.from(5678) 126 | 127 | await expect(safeMathInt.div(a, b)) 128 | .to.emit(safeMathInt, 'ReturnValueInt256') 129 | .withArgs(a.div(b)) 130 | }) 131 | 132 | it('should fail on zero division', async function () { 133 | const a = ethers.BigNumber.from(5678) 134 | const b = ethers.BigNumber.from(0) 135 | 136 | await expect(safeMathInt.div(a, b)).to.be.reverted 137 | }) 138 | 139 | it('should fail when MIN_INT256 is divided by -1', async function () { 140 | const a = ethers.BigNumber.from(MIN_INT256) 141 | const b = ethers.BigNumber.from(-1) 142 | 143 | await expect(safeMathInt.div(a, b)).to.be.reverted 144 | }) 145 | }) 146 | 147 | describe('abs', function () { 148 | it('works for 0', async function () { 149 | await expect(safeMathInt.abs(0)) 150 | .to.emit(safeMathInt, 'ReturnValueInt256') 151 | .withArgs(0) 152 | }) 153 | 154 | it('works on positive numbers', async function () { 155 | await expect(safeMathInt.abs(100)) 156 | .to.emit(safeMathInt, 'ReturnValueInt256') 157 | .withArgs(100) 158 | }) 159 | 160 | it('works on negative numbers', async function () { 161 | await expect(safeMathInt.abs(-100)) 162 | .to.emit(safeMathInt, 'ReturnValueInt256') 163 | .withArgs(100) 164 | }) 165 | 166 | it('fails on overflow condition', async function () { 167 | await expect(safeMathInt.abs(MIN_INT256)).to.be.reverted 168 | }) 169 | }) 170 | describe('twoPower', function () { 171 | const decimals18 = ethers.BigNumber.from('1000000000000000000') 172 | const decimals10 = ethers.BigNumber.from('10000000000') 173 | it('2^0', async function () { 174 | const e = ethers.BigNumber.from(0) 175 | const one = ethers.BigNumber.from(1).mul(decimals18) 176 | await expect(safeMathInt.twoPower(e, one)) 177 | .to.emit(safeMathInt, 'ReturnValueInt256') 178 | .withArgs(one) 179 | }) 180 | it('2^1', async function () { 181 | const e = ethers.BigNumber.from(1).mul(decimals18) 182 | const one = ethers.BigNumber.from(1).mul(decimals18) 183 | const result = ethers.BigNumber.from(2).mul(decimals18) 184 | ;(await expect(safeMathInt.twoPower(e, one))).to 185 | .emit(safeMathInt, 'ReturnValueInt256') 186 | .withArgs(result) 187 | }) 188 | it('2^30', async function () { 189 | const e = ethers.BigNumber.from(30).mul(decimals18) 190 | const one = ethers.BigNumber.from(1).mul(decimals18) 191 | const result = ethers.BigNumber.from(2 ** 30).mul(decimals18) 192 | ;(await expect(safeMathInt.twoPower(e, one))).to 193 | .emit(safeMathInt, 'ReturnValueInt256') 194 | .withArgs(result) 195 | }) 196 | it('2^2.5', async function () { 197 | const e = ethers.BigNumber.from('25000000000') 198 | const one = ethers.BigNumber.from(1).mul(decimals10) 199 | const result = ethers.BigNumber.from('56568542494') 200 | ;(await expect(safeMathInt.twoPower(e, one))).to 201 | .emit(safeMathInt, 'ReturnValueInt256') 202 | .withArgs(result) 203 | }) 204 | it('2^2.25', async function () { 205 | const e = ethers.BigNumber.from('22500000000') 206 | const one = ethers.BigNumber.from(1).mul(decimals10) 207 | const result = ethers.BigNumber.from('47568284600') 208 | ;(await expect(safeMathInt.twoPower(e, one))).to 209 | .emit(safeMathInt, 'ReturnValueInt256') 210 | .withArgs(result) 211 | }) 212 | it('2^-2.25', async function () { 213 | const e = ethers.BigNumber.from('-22500000000') 214 | const one = ethers.BigNumber.from(1).mul(decimals10) 215 | const result = ethers.BigNumber.from('2102241038') 216 | ;(await expect(safeMathInt.twoPower(e, one))).to 217 | .emit(safeMathInt, 'ReturnValueInt256') 218 | .withArgs(result) 219 | }) 220 | it('2^-0.6', async function () { 221 | const e = ethers.BigNumber.from('-6000000000') 222 | const one = ethers.BigNumber.from(1).mul(decimals10) 223 | const result = ethers.BigNumber.from('6626183216') 224 | ;(await expect(safeMathInt.twoPower(e, one))).to 225 | .emit(safeMathInt, 'ReturnValueInt256') 226 | .withArgs(result) 227 | }) 228 | it('2^2.96875', async function () { 229 | const e = ethers.BigNumber.from('29687500000') 230 | const one = ethers.BigNumber.from(1).mul(decimals10) 231 | const result = ethers.BigNumber.from('78285764964') 232 | ;(await expect(safeMathInt.twoPower(e, one))).to 233 | .emit(safeMathInt, 'ReturnValueInt256') 234 | .withArgs(result) 235 | }) 236 | it('2^2.99', async function () { 237 | const e = ethers.BigNumber.from('29900000000') 238 | const one = ethers.BigNumber.from(1).mul(decimals10) 239 | const result = ethers.BigNumber.from('78285764964') 240 | ;(await expect(safeMathInt.twoPower(e, one))).to 241 | .emit(safeMathInt, 'ReturnValueInt256') 242 | .withArgs(result) 243 | }) 244 | it('should fail on too small exponents', async function () { 245 | const e = ethers.BigNumber.from('-1011000000000') 246 | const one = ethers.BigNumber.from(1).mul(decimals10) 247 | await expect(safeMathInt.twoPower(e, one)).to.be.reverted 248 | }) 249 | it('should fail on too large exponents', async function () { 250 | const e = ethers.BigNumber.from('1011000000000') 251 | const one = ethers.BigNumber.from(1).mul(decimals10) 252 | await expect(safeMathInt.twoPower(e, one)).to.be.reverted 253 | }) 254 | }) 255 | }) 256 | -------------------------------------------------------------------------------- /contracts/WAMPL.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 5 | // solhint-disable-next-line max-line-length 6 | import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 7 | import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 8 | 9 | // solhint-disable-next-line max-line-length 10 | import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; 11 | 12 | /** 13 | * @title WAMPL (Wrapped AMPL). 14 | * 15 | * @dev A fixed-balance ERC-20 wrapper for the AMPL rebasing token. 16 | * 17 | * Users deposit AMPL into this contract and are minted wAMPL. 18 | * 19 | * Each account's wAMPL balance represents the fixed percentage ownership 20 | * of AMPL's market cap. 21 | * 22 | * For example: 100K wAMPL => 1% of the AMPL market cap 23 | * when the AMPL supply is 100M, 100K wAMPL will be redeemable for 1M AMPL 24 | * when the AMPL supply is 500M, 100K wAMPL will be redeemable for 5M AMPL 25 | * and so on. 26 | * 27 | * We call wAMPL the "wrapper" token and AMPL the "underlying" or "wrapped" token. 28 | */ 29 | contract WAMPL is ERC20Upgradeable, ERC20PermitUpgradeable { 30 | using SafeERC20Upgradeable for IERC20Upgradeable; 31 | 32 | //-------------------------------------------------------------------------- 33 | // Constants 34 | 35 | /// @dev The maximum wAMPL supply. 36 | uint256 public constant MAX_WAMPL_SUPPLY = 10000000 * (10**18); // 10 M 37 | 38 | //-------------------------------------------------------------------------- 39 | // Attributes 40 | 41 | /// @dev The reference to the AMPL token. 42 | address private immutable _ampl; 43 | 44 | //-------------------------------------------------------------------------- 45 | 46 | /// @notice Contract constructor. 47 | /// @param ampl The AMPL ERC20 token address. 48 | constructor(address ampl) { 49 | _ampl = ampl; 50 | } 51 | 52 | /// @notice Contract state initialization. 53 | /// @param name_ The wAMPL ERC20 name. 54 | /// @param symbol_ The wAMPL ERC20 symbol. 55 | function init(string memory name_, string memory symbol_) public initializer { 56 | __ERC20_init(name_, symbol_); 57 | __ERC20Permit_init(name_); 58 | } 59 | 60 | //-------------------------------------------------------------------------- 61 | // WAMPL write methods 62 | 63 | /// @notice Transfers AMPLs from {msg.sender} and mints wAMPLs. 64 | /// 65 | /// @param wamples The amount of wAMPLs to mint. 66 | /// @return The amount of AMPLs deposited. 67 | function mint(uint256 wamples) external returns (uint256) { 68 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 69 | _deposit(_msgSender(), _msgSender(), amples, wamples); 70 | return amples; 71 | } 72 | 73 | /// @notice Transfers AMPLs from {msg.sender} and mints wAMPLs, 74 | /// to the specified beneficiary. 75 | /// 76 | /// @param to The beneficiary wallet. 77 | /// @param wamples The amount of wAMPLs to mint. 78 | /// @return The amount of AMPLs deposited. 79 | function mintFor(address to, uint256 wamples) external returns (uint256) { 80 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 81 | _deposit(_msgSender(), to, amples, wamples); 82 | return amples; 83 | } 84 | 85 | /// @notice Burns wAMPLs from {msg.sender} and transfers AMPLs back. 86 | /// 87 | /// @param wamples The amount of wAMPLs to burn. 88 | /// @return The amount of AMPLs withdrawn. 89 | function burn(uint256 wamples) external returns (uint256) { 90 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 91 | _withdraw(_msgSender(), _msgSender(), amples, wamples); 92 | return amples; 93 | } 94 | 95 | /// @notice Burns wAMPLs from {msg.sender} and transfers AMPLs back, 96 | /// to the specified beneficiary. 97 | /// 98 | /// @param to The beneficiary wallet. 99 | /// @param wamples The amount of wAMPLs to burn. 100 | /// @return The amount of AMPLs withdrawn. 101 | function burnTo(address to, uint256 wamples) external returns (uint256) { 102 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 103 | _withdraw(_msgSender(), to, amples, wamples); 104 | return amples; 105 | } 106 | 107 | /// @notice Burns all wAMPLs from {msg.sender} and transfers AMPLs back. 108 | /// 109 | /// @return The amount of AMPLs withdrawn. 110 | function burnAll() external returns (uint256) { 111 | uint256 wamples = balanceOf(_msgSender()); 112 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 113 | _withdraw(_msgSender(), _msgSender(), amples, wamples); 114 | return amples; 115 | } 116 | 117 | /// @notice Burns all wAMPLs from {msg.sender} and transfers AMPLs back, 118 | /// to the specified beneficiary. 119 | /// 120 | /// @param to The beneficiary wallet. 121 | /// @return The amount of AMPLs withdrawn. 122 | function burnAllTo(address to) external returns (uint256) { 123 | uint256 wamples = balanceOf(_msgSender()); 124 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 125 | _withdraw(_msgSender(), to, amples, wamples); 126 | return amples; 127 | } 128 | 129 | /// @notice Transfers AMPLs from {msg.sender} and mints wAMPLs. 130 | /// 131 | /// @param amples The amount of AMPLs to deposit. 132 | /// @return The amount of wAMPLs minted. 133 | function deposit(uint256 amples) external returns (uint256) { 134 | uint256 wamples = _ampleToWample(amples, _queryAMPLSupply()); 135 | _deposit(_msgSender(), _msgSender(), amples, wamples); 136 | return wamples; 137 | } 138 | 139 | /// @notice Transfers AMPLs from {msg.sender} and mints wAMPLs, 140 | /// to the specified beneficiary. 141 | /// 142 | /// @param to The beneficiary wallet. 143 | /// @param amples The amount of AMPLs to deposit. 144 | /// @return The amount of wAMPLs minted. 145 | function depositFor(address to, uint256 amples) external returns (uint256) { 146 | uint256 wamples = _ampleToWample(amples, _queryAMPLSupply()); 147 | _deposit(_msgSender(), to, amples, wamples); 148 | return wamples; 149 | } 150 | 151 | /// @notice Burns wAMPLs from {msg.sender} and transfers AMPLs back. 152 | /// 153 | /// @param amples The amount of AMPLs to withdraw. 154 | /// @return The amount of burnt wAMPLs. 155 | function withdraw(uint256 amples) external returns (uint256) { 156 | uint256 wamples = _ampleToWample(amples, _queryAMPLSupply()); 157 | _withdraw(_msgSender(), _msgSender(), amples, wamples); 158 | return wamples; 159 | } 160 | 161 | /// @notice Burns wAMPLs from {msg.sender} and transfers AMPLs back, 162 | /// to the specified beneficiary. 163 | /// 164 | /// @param to The beneficiary wallet. 165 | /// @param amples The amount of AMPLs to withdraw. 166 | /// @return The amount of burnt wAMPLs. 167 | function withdrawTo(address to, uint256 amples) external returns (uint256) { 168 | uint256 wamples = _ampleToWample(amples, _queryAMPLSupply()); 169 | _withdraw(_msgSender(), to, amples, wamples); 170 | return wamples; 171 | } 172 | 173 | /// @notice Burns all wAMPLs from {msg.sender} and transfers AMPLs back. 174 | /// 175 | /// @return The amount of burnt wAMPLs. 176 | function withdrawAll() external returns (uint256) { 177 | uint256 wamples = balanceOf(_msgSender()); 178 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 179 | _withdraw(_msgSender(), _msgSender(), amples, wamples); 180 | return wamples; 181 | } 182 | 183 | /// @notice Burns all wAMPLs from {msg.sender} and transfers AMPLs back, 184 | /// to the specified beneficiary. 185 | /// 186 | /// @param to The beneficiary wallet. 187 | /// @return The amount of burnt wAMPLs. 188 | function withdrawAllTo(address to) external returns (uint256) { 189 | uint256 wamples = balanceOf(_msgSender()); 190 | uint256 amples = _wampleToAmple(wamples, _queryAMPLSupply()); 191 | _withdraw(_msgSender(), to, amples, wamples); 192 | return wamples; 193 | } 194 | 195 | //-------------------------------------------------------------------------- 196 | // WAMPL view methods 197 | 198 | /// @return The address of the underlying "wrapped" token ie) AMPL. 199 | function underlying() external view returns (address) { 200 | return _ampl; 201 | } 202 | 203 | /// @return The total AMPLs held by this contract. 204 | function totalUnderlying() external view returns (uint256) { 205 | return _wampleToAmple(totalSupply(), _queryAMPLSupply()); 206 | } 207 | 208 | /// @param owner The account address. 209 | /// @return The AMPL balance redeemable by the owner. 210 | function balanceOfUnderlying(address owner) external view returns (uint256) { 211 | return _wampleToAmple(balanceOf(owner), _queryAMPLSupply()); 212 | } 213 | 214 | /// @param amples The amount of AMPL tokens. 215 | /// @return The amount of wAMPL tokens exchangeable. 216 | function underlyingToWrapper(uint256 amples) external view returns (uint256) { 217 | return _ampleToWample(amples, _queryAMPLSupply()); 218 | } 219 | 220 | /// @param wamples The amount of wAMPL tokens. 221 | /// @return The amount of AMPL tokens exchangeable. 222 | function wrapperToUnderlying(uint256 wamples) external view returns (uint256) { 223 | return _wampleToAmple(wamples, _queryAMPLSupply()); 224 | } 225 | 226 | //-------------------------------------------------------------------------- 227 | // Private methods 228 | 229 | /// @dev Internal helper function to handle deposit state change. 230 | /// @param from The initiator wallet. 231 | /// @param to The beneficiary wallet. 232 | /// @param amples The amount of AMPLs to deposit. 233 | /// @param wamples The amount of wAMPLs to mint. 234 | function _deposit( 235 | address from, 236 | address to, 237 | uint256 amples, 238 | uint256 wamples 239 | ) private { 240 | IERC20Upgradeable(_ampl).safeTransferFrom(from, address(this), amples); 241 | 242 | _mint(to, wamples); 243 | } 244 | 245 | /// @dev Internal helper function to handle withdraw state change. 246 | /// @param from The initiator wallet. 247 | /// @param to The beneficiary wallet. 248 | /// @param amples The amount of AMPLs to withdraw. 249 | /// @param wamples The amount of wAMPLs to burn. 250 | function _withdraw( 251 | address from, 252 | address to, 253 | uint256 amples, 254 | uint256 wamples 255 | ) private { 256 | _burn(from, wamples); 257 | 258 | IERC20Upgradeable(_ampl).safeTransfer(to, amples); 259 | } 260 | 261 | /// @dev Queries the current total supply of AMPL. 262 | /// @return The current AMPL supply. 263 | function _queryAMPLSupply() private view returns (uint256) { 264 | return IERC20Upgradeable(_ampl).totalSupply(); 265 | } 266 | 267 | //-------------------------------------------------------------------------- 268 | // Pure methods 269 | 270 | /// @dev Converts AMPLs to wAMPL amount. 271 | function _ampleToWample(uint256 amples, uint256 totalAMPLSupply) 272 | private 273 | pure 274 | returns (uint256) 275 | { 276 | return (amples * MAX_WAMPL_SUPPLY) / totalAMPLSupply; 277 | } 278 | 279 | /// @dev Converts wAMPLs amount to AMPLs. 280 | function _wampleToAmple(uint256 wamples, uint256 totalAMPLSupply) 281 | private 282 | pure 283 | returns (uint256) 284 | { 285 | return (wamples * totalAMPLSupply) / MAX_WAMPL_SUPPLY; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /test/unit/Orchestrator.ts: -------------------------------------------------------------------------------- 1 | import { ethers, waffle } from 'hardhat' 2 | import { Contract, Signer } from 'ethers' 3 | import { increaseTime } from '../utils/utils' 4 | import { expect } from 'chai' 5 | import { TransactionResponse } from '@ethersproject/providers' 6 | 7 | let orchestrator: Contract, mockPolicy: Contract, mockDownstream: Contract 8 | let r: Promise 9 | let deployer: Signer, user: Signer 10 | 11 | async function mockedOrchestrator() { 12 | await increaseTime(86400) 13 | // get signers 14 | const [deployer, user] = await ethers.getSigners() 15 | // deploy mocks 16 | const mockPolicy = await ( 17 | await ethers.getContractFactory('MockUFragmentsPolicy') 18 | ) 19 | .connect(deployer) 20 | .deploy() 21 | const orchestrator = await (await ethers.getContractFactory('Orchestrator')) 22 | .connect(deployer) 23 | .deploy(mockPolicy.address) 24 | const mockDownstream = await ( 25 | await ethers.getContractFactory('MockDownstream') 26 | ) 27 | .connect(deployer) 28 | .deploy() 29 | return { 30 | deployer, 31 | user, 32 | orchestrator, 33 | mockPolicy, 34 | mockDownstream, 35 | } 36 | } 37 | 38 | describe('Orchestrator', function () { 39 | before('setup Orchestrator contract', async () => { 40 | ;({ deployer, user, orchestrator, mockPolicy, mockDownstream } = 41 | await waffle.loadFixture(mockedOrchestrator)) 42 | }) 43 | 44 | describe('when sent ether', async function () { 45 | it('should reject', async function () { 46 | await expect(user.sendTransaction({ to: orchestrator.address, value: 1 })) 47 | .to.be.reverted 48 | }) 49 | }) 50 | 51 | describe('when rebase called by a contract', function () { 52 | it('should fail', async function () { 53 | const rebaseCallerContract = await ( 54 | await ethers.getContractFactory('RebaseCallerContract') 55 | ) 56 | .connect(deployer) 57 | .deploy() 58 | await expect(rebaseCallerContract.callRebase(orchestrator.address)).to.be 59 | .reverted 60 | }) 61 | }) 62 | 63 | describe('when rebase called by a contract which is being constructed', function () { 64 | it('should fail', async function () { 65 | await expect( 66 | (await ethers.getContractFactory('ConstructorRebaseCallerContract')) 67 | .connect(deployer) 68 | .deploy(orchestrator.address), 69 | ).to.be.reverted 70 | }) 71 | }) 72 | 73 | describe('when transaction list is empty', async function () { 74 | before('calling rebase', async function () { 75 | r = orchestrator.rebase() 76 | }) 77 | 78 | it('should have no transactions', async function () { 79 | expect(await orchestrator.transactionsSize()).to.eq(0) 80 | }) 81 | 82 | it('should call rebase on policy', async function () { 83 | await expect(r) 84 | .to.emit(mockPolicy, 'FunctionCalled') 85 | .withArgs('UFragmentsPolicy', 'rebase', orchestrator.address) 86 | }) 87 | 88 | it('should not have any subsequent logs', async function () { 89 | expect((await (await r).wait()).logs.length).to.eq(1) 90 | }) 91 | }) 92 | 93 | describe('when there is a single transaction', async function () { 94 | before('adding a transaction', async function () { 95 | const updateOneArgEncoded = 96 | await mockDownstream.populateTransaction.updateOneArg(12345) 97 | await orchestrator 98 | .connect(deployer) 99 | .addTransaction(mockDownstream.address, updateOneArgEncoded.data) 100 | r = orchestrator.connect(deployer).rebase() 101 | }) 102 | 103 | it('should have 1 transaction', async function () { 104 | expect(await orchestrator.transactionsSize()).to.eq(1) 105 | }) 106 | 107 | it('should call rebase on policy', async function () { 108 | await expect(r) 109 | .to.emit(mockPolicy, 'FunctionCalled') 110 | .withArgs('UFragmentsPolicy', 'rebase', orchestrator.address) 111 | }) 112 | 113 | it('should call the transaction', async function () { 114 | await expect(r) 115 | .to.emit(mockDownstream, 'FunctionCalled') 116 | .withArgs('MockDownstream', 'updateOneArg', orchestrator.address) 117 | 118 | await expect(r) 119 | .to.emit(mockDownstream, 'FunctionArguments') 120 | .withArgs([12345], []) 121 | }) 122 | 123 | it('should not have any subsequent logs', async function () { 124 | expect((await (await r).wait()).logs.length).to.eq(3) 125 | }) 126 | }) 127 | 128 | describe('when there are two transactions', async function () { 129 | before('adding a transaction', async function () { 130 | const updateTwoArgsEncoded = 131 | await mockDownstream.populateTransaction.updateTwoArgs(12345, 23456) 132 | await orchestrator 133 | .connect(deployer) 134 | .addTransaction(mockDownstream.address, updateTwoArgsEncoded.data) 135 | r = orchestrator.connect(deployer).rebase() 136 | }) 137 | 138 | it('should have 2 transactions', async function () { 139 | expect(await orchestrator.transactionsSize()).to.eq(2) 140 | }) 141 | 142 | it('should call rebase on policy', async function () { 143 | await expect(r) 144 | .to.emit(mockPolicy, 'FunctionCalled') 145 | .withArgs('UFragmentsPolicy', 'rebase', orchestrator.address) 146 | }) 147 | 148 | it('should call first transaction', async function () { 149 | await expect(r) 150 | .to.emit(mockDownstream, 'FunctionCalled') 151 | .withArgs('MockDownstream', 'updateOneArg', orchestrator.address) 152 | 153 | await expect(r) 154 | .to.emit(mockDownstream, 'FunctionArguments') 155 | .withArgs([12345], []) 156 | }) 157 | 158 | it('should call second transaction', async function () { 159 | await expect(r) 160 | .to.emit(mockDownstream, 'FunctionCalled') 161 | .withArgs('MockDownstream', 'updateTwoArgs', orchestrator.address) 162 | 163 | await expect(r) 164 | .to.emit(mockDownstream, 'FunctionArguments') 165 | .withArgs([12345], [23456]) 166 | }) 167 | 168 | it('should not have any subsequent logs', async function () { 169 | expect((await (await r).wait()).logs.length).to.eq(5) 170 | }) 171 | }) 172 | 173 | describe('when 1st transaction is disabled', async function () { 174 | before('disabling a transaction', async function () { 175 | await orchestrator.connect(deployer).setTransactionEnabled(0, false) 176 | r = orchestrator.connect(deployer).rebase() 177 | }) 178 | 179 | it('should have 2 transactions', async function () { 180 | expect(await orchestrator.transactionsSize()).to.eq(2) 181 | }) 182 | 183 | it('should call rebase on policy', async function () { 184 | await expect(r) 185 | .to.emit(mockPolicy, 'FunctionCalled') 186 | .withArgs('UFragmentsPolicy', 'rebase', orchestrator.address) 187 | }) 188 | 189 | it('should call second transaction', async function () { 190 | await expect(r) 191 | .to.emit(mockDownstream, 'FunctionCalled') 192 | .withArgs('MockDownstream', 'updateTwoArgs', orchestrator.address) 193 | 194 | await expect(r) 195 | .to.emit(mockDownstream, 'FunctionArguments') 196 | .withArgs([12345], [23456]) 197 | }) 198 | 199 | it('should not have any subsequent logs', async function () { 200 | expect(await (await (await r).wait()).logs.length).to.eq(3) 201 | }) 202 | }) 203 | 204 | describe('when a transaction is removed', async function () { 205 | before('removing 1st transaction', async function () { 206 | await orchestrator.connect(deployer).removeTransaction(0) 207 | r = orchestrator.connect(deployer).rebase() 208 | }) 209 | 210 | it('should have 1 transaction', async function () { 211 | expect(await orchestrator.transactionsSize()).to.eq(1) 212 | }) 213 | 214 | it('should call rebase on policy', async function () { 215 | await expect(r) 216 | .to.emit(mockPolicy, 'FunctionCalled') 217 | .withArgs('UFragmentsPolicy', 'rebase', orchestrator.address) 218 | }) 219 | 220 | it('should call the transaction', async function () { 221 | await expect(r) 222 | .to.emit(mockDownstream, 'FunctionCalled') 223 | .withArgs('MockDownstream', 'updateTwoArgs', orchestrator.address) 224 | 225 | await expect(r) 226 | .to.emit(mockDownstream, 'FunctionArguments') 227 | .withArgs([12345], [23456]) 228 | }) 229 | 230 | it('should not have any subsequent logs', async function () { 231 | expect((await (await r).wait()).logs.length).to.eq(3) 232 | }) 233 | }) 234 | 235 | describe('when all transactions are removed', async function () { 236 | before('removing 1st transaction', async function () { 237 | await orchestrator.connect(deployer).removeTransaction(0) 238 | r = orchestrator.connect(deployer).rebase() 239 | }) 240 | 241 | it('should have 0 transactions', async function () { 242 | expect(await orchestrator.transactionsSize()).to.eq(0) 243 | }) 244 | 245 | it('should call rebase on policy', async function () { 246 | await expect(r) 247 | .to.emit(mockPolicy, 'FunctionCalled') 248 | .withArgs('UFragmentsPolicy', 'rebase', orchestrator.address) 249 | }) 250 | 251 | it('should not have any subsequent logs', async function () { 252 | expect((await (await r).wait()).logs.length).to.eq(1) 253 | }) 254 | }) 255 | 256 | describe('when a transaction reverts', async function () { 257 | before('adding 3 transactions', async function () { 258 | const updateOneArgEncoded = 259 | await mockDownstream.populateTransaction.updateOneArg(123) 260 | await orchestrator 261 | .connect(deployer) 262 | .addTransaction(mockDownstream.address, updateOneArgEncoded.data) 263 | 264 | const revertsEncoded = await mockDownstream.populateTransaction.reverts() 265 | await orchestrator 266 | .connect(deployer) 267 | .addTransaction(mockDownstream.address, revertsEncoded.data) 268 | 269 | const updateTwoArgsEncoded = 270 | await mockDownstream.populateTransaction.updateTwoArgs(12345, 23456) 271 | await orchestrator 272 | .connect(deployer) 273 | .addTransaction(mockDownstream.address, updateTwoArgsEncoded.data) 274 | await expect(orchestrator.connect(deployer).rebase()).to.be.reverted 275 | }) 276 | 277 | it('should have 3 transactions', async function () { 278 | expect(await orchestrator.transactionsSize()).to.eq(3) 279 | }) 280 | }) 281 | 282 | describe('Access Control', function () { 283 | describe('addTransaction', async function () { 284 | it('should be callable by owner', async function () { 285 | const updateNoArgEncoded = 286 | await mockDownstream.populateTransaction.updateNoArg() 287 | await expect( 288 | orchestrator 289 | .connect(deployer) 290 | .addTransaction(mockDownstream.address, updateNoArgEncoded.data), 291 | ).to.not.be.reverted 292 | }) 293 | 294 | it('should not be callable by others', async function () { 295 | const updateNoArgEncoded = 296 | await mockDownstream.populateTransaction.updateNoArg() 297 | await expect( 298 | orchestrator 299 | .connect(user) 300 | .addTransaction(mockDownstream.address, updateNoArgEncoded.data), 301 | ).to.be.reverted 302 | }) 303 | }) 304 | 305 | describe('setTransactionEnabled', async function () { 306 | it('should be callable by owner', async function () { 307 | expect(await orchestrator.transactionsSize()).to.gt(0) 308 | await expect( 309 | orchestrator.connect(deployer).setTransactionEnabled(0, true), 310 | ).to.not.be.reverted 311 | }) 312 | 313 | it('should revert if index out of bounds', async function () { 314 | expect(await orchestrator.transactionsSize()).to.lt(5) 315 | await expect( 316 | orchestrator.connect(deployer).setTransactionEnabled(5, true), 317 | ).to.be.reverted 318 | }) 319 | 320 | it('should not be callable by others', async function () { 321 | expect(await orchestrator.transactionsSize()).to.gt(0) 322 | await expect(orchestrator.connect(user).setTransactionEnabled(0, true)) 323 | .to.be.reverted 324 | }) 325 | }) 326 | 327 | describe('removeTransaction', async function () { 328 | it('should not be callable by others', async function () { 329 | expect(await orchestrator.transactionsSize()).to.gt(0) 330 | await expect(orchestrator.connect(user).removeTransaction(0)).to.be 331 | .reverted 332 | }) 333 | 334 | it('should revert if index out of bounds', async function () { 335 | expect(await orchestrator.transactionsSize()).to.lt(5) 336 | await expect(orchestrator.connect(deployer).removeTransaction(5)).to.be 337 | .reverted 338 | }) 339 | 340 | it('should be callable by owner', async function () { 341 | expect(await orchestrator.transactionsSize()).to.gt(0) 342 | await expect(orchestrator.connect(deployer).removeTransaction(0)).to.not 343 | .be.reverted 344 | }) 345 | }) 346 | 347 | describe('transferOwnership', async function () { 348 | it('should transfer ownership', async function () { 349 | expect(await orchestrator.owner()).to.eq(await deployer.getAddress()) 350 | await orchestrator 351 | .connect(deployer) 352 | .transferOwnership(user.getAddress()) 353 | expect(await orchestrator.owner()).to.eq(await user.getAddress()) 354 | }) 355 | }) 356 | }) 357 | }) 358 | -------------------------------------------------------------------------------- /contracts/UFragmentsPolicy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | import "./_external/SafeMath.sol"; 5 | import "./_external/Ownable.sol"; 6 | 7 | import "./lib/SafeMathInt.sol"; 8 | import "./lib/UInt256Lib.sol"; 9 | 10 | interface IUFragments { 11 | function totalSupply() external view returns (uint256); 12 | 13 | function rebase(uint256 epoch, int256 supplyDelta) external returns (uint256); 14 | } 15 | 16 | interface IOracle { 17 | function getData() external returns (uint256, bool); 18 | } 19 | 20 | /** 21 | * @title uFragments Monetary Supply Policy 22 | * @dev This is an implementation of the uFragments Ideal Money protocol. 23 | * 24 | * This component regulates the token supply of the uFragments ERC20 token in response to 25 | * market oracles. 26 | */ 27 | contract UFragmentsPolicy is Ownable { 28 | using SafeMath for uint256; 29 | using SafeMathInt for int256; 30 | using UInt256Lib for uint256; 31 | 32 | event LogRebase( 33 | uint256 indexed epoch, 34 | uint256 exchangeRate, 35 | uint256 cpi, 36 | int256 requestedSupplyAdjustment, 37 | uint256 timestampSec 38 | ); 39 | 40 | IUFragments public uFrags; 41 | 42 | // Provides the current CPI, as an 18 decimal fixed point number. 43 | IOracle public cpiOracle; 44 | 45 | // Market oracle provides the token/USD exchange rate as an 18 decimal fixed point number. 46 | // (eg) An oracle value of 1.5e18 it would mean 1 Ample is trading for $1.50. 47 | IOracle public marketOracle; 48 | 49 | // CPI value at the time of launch, as an 18 decimal fixed point number. 50 | uint256 private baseCpi; 51 | 52 | // If the current exchange rate is within this fractional distance from the target, no supply 53 | // update is performed. Fixed point number--same format as the rate. 54 | // (ie) abs(rate - targetRate) / targetRate < deviationThreshold, then no supply change. 55 | // DECIMALS Fixed point number. 56 | uint256 public deviationThreshold; 57 | 58 | uint256 private rebaseLagDeprecated; 59 | 60 | // More than this much time must pass between rebase operations. 61 | uint256 public minRebaseTimeIntervalSec; 62 | 63 | // Block timestamp of last rebase operation 64 | uint256 public lastRebaseTimestampSec; 65 | 66 | // The rebase window begins this many seconds into the minRebaseTimeInterval period. 67 | // For example if minRebaseTimeInterval is 24hrs, it represents the time of day in seconds. 68 | uint256 public rebaseWindowOffsetSec; 69 | 70 | // The length of the time window where a rebase operation is allowed to execute, in seconds. 71 | uint256 public rebaseWindowLengthSec; 72 | 73 | // The number of rebase cycles since inception 74 | uint256 public epoch; 75 | 76 | uint256 private constant DECIMALS = 18; 77 | 78 | // Due to the expression in computeSupplyDelta(), MAX_RATE * MAX_SUPPLY must fit into an int256. 79 | // Both are 18 decimals fixed point numbers. 80 | uint256 private constant MAX_RATE = 10**6 * 10**DECIMALS; 81 | // MAX_SUPPLY = MAX_INT256 / MAX_RATE 82 | uint256 private constant MAX_SUPPLY = uint256(type(int256).max) / MAX_RATE; 83 | 84 | // This module orchestrates the rebase execution and downstream notification. 85 | address public orchestrator; 86 | 87 | // DECIMALS decimal fixed point numbers. 88 | // Used in computation of (Upper-Lower)/(1-(Upper/Lower)/2^(Growth*delta))) + Lower 89 | int256 public rebaseFunctionLowerPercentage; 90 | int256 public rebaseFunctionUpperPercentage; 91 | int256 public rebaseFunctionGrowth; 92 | 93 | int256 private constant ONE = int256(10**DECIMALS); 94 | 95 | modifier onlyOrchestrator() { 96 | require(msg.sender == orchestrator); 97 | _; 98 | } 99 | 100 | /** 101 | * @notice Initiates a new rebase operation, provided the minimum time period has elapsed. 102 | * @dev Changes supply with percentage of: 103 | * (Upper-Lower)/(1-(Upper/Lower)/2^(Growth*NormalizedPriceDelta))) + Lower 104 | */ 105 | function rebase() external onlyOrchestrator { 106 | require(inRebaseWindow()); 107 | 108 | // This comparison also ensures there is no reentrancy. 109 | require(lastRebaseTimestampSec.add(minRebaseTimeIntervalSec) < block.timestamp); 110 | 111 | // Snap the rebase time to the start of this window. 112 | lastRebaseTimestampSec = block 113 | .timestamp 114 | .sub(block.timestamp.mod(minRebaseTimeIntervalSec)) 115 | .add(rebaseWindowOffsetSec); 116 | 117 | epoch = epoch.add(1); 118 | 119 | uint256 cpi; 120 | bool cpiValid; 121 | (cpi, cpiValid) = cpiOracle.getData(); 122 | require(cpiValid); 123 | 124 | uint256 targetRate = cpi.mul(10**DECIMALS).div(baseCpi); 125 | 126 | uint256 exchangeRate; 127 | bool rateValid; 128 | (exchangeRate, rateValid) = marketOracle.getData(); 129 | require(rateValid); 130 | 131 | if (exchangeRate > MAX_RATE) { 132 | exchangeRate = MAX_RATE; 133 | } 134 | 135 | int256 supplyDelta = computeSupplyDelta(exchangeRate, targetRate); 136 | 137 | if (supplyDelta > 0 && uFrags.totalSupply().add(uint256(supplyDelta)) > MAX_SUPPLY) { 138 | supplyDelta = (MAX_SUPPLY.sub(uFrags.totalSupply())).toInt256Safe(); 139 | } 140 | 141 | uint256 supplyAfterRebase = uFrags.rebase(epoch, supplyDelta); 142 | assert(supplyAfterRebase <= MAX_SUPPLY); 143 | emit LogRebase(epoch, exchangeRate, cpi, supplyDelta, block.timestamp); 144 | } 145 | 146 | /** 147 | * @notice Sets the reference to the CPI oracle. 148 | * @param cpiOracle_ The address of the cpi oracle contract. 149 | */ 150 | function setCpiOracle(IOracle cpiOracle_) external onlyOwner { 151 | cpiOracle = cpiOracle_; 152 | } 153 | 154 | /** 155 | * @notice Sets the reference to the market oracle. 156 | * @param marketOracle_ The address of the market oracle contract. 157 | */ 158 | function setMarketOracle(IOracle marketOracle_) external onlyOwner { 159 | marketOracle = marketOracle_; 160 | } 161 | 162 | /** 163 | * @notice Sets the reference to the orchestrator. 164 | * @param orchestrator_ The address of the orchestrator contract. 165 | */ 166 | function setOrchestrator(address orchestrator_) external onlyOwner { 167 | orchestrator = orchestrator_; 168 | } 169 | 170 | function setRebaseFunctionGrowth(int256 rebaseFunctionGrowth_) external onlyOwner { 171 | require(rebaseFunctionGrowth_ >= 0); 172 | rebaseFunctionGrowth = rebaseFunctionGrowth_; 173 | } 174 | 175 | function setRebaseFunctionLowerPercentage(int256 rebaseFunctionLowerPercentage_) 176 | external 177 | onlyOwner 178 | { 179 | require(rebaseFunctionLowerPercentage_ <= 0); 180 | rebaseFunctionLowerPercentage = rebaseFunctionLowerPercentage_; 181 | } 182 | 183 | function setRebaseFunctionUpperPercentage(int256 rebaseFunctionUpperPercentage_) 184 | external 185 | onlyOwner 186 | { 187 | require(rebaseFunctionUpperPercentage_ >= 0); 188 | rebaseFunctionUpperPercentage = rebaseFunctionUpperPercentage_; 189 | } 190 | 191 | /** 192 | * @notice Sets the deviation threshold fraction. If the exchange rate given by the market 193 | * oracle is within this fractional distance from the targetRate, then no supply 194 | * modifications are made. DECIMALS fixed point number. 195 | * @param deviationThreshold_ The new exchange rate threshold fraction. 196 | */ 197 | function setDeviationThreshold(uint256 deviationThreshold_) external onlyOwner { 198 | deviationThreshold = deviationThreshold_; 199 | } 200 | 201 | /** 202 | * @notice Sets the parameters which control the timing and frequency of 203 | * rebase operations. 204 | * a) the minimum time period that must elapse between rebase cycles. 205 | * b) the rebase window offset parameter. 206 | * c) the rebase window length parameter. 207 | * @param minRebaseTimeIntervalSec_ More than this much time must pass between rebase 208 | * operations, in seconds. 209 | * @param rebaseWindowOffsetSec_ The number of seconds from the beginning of 210 | the rebase interval, where the rebase window begins. 211 | * @param rebaseWindowLengthSec_ The length of the rebase window in seconds. 212 | */ 213 | function setRebaseTimingParameters( 214 | uint256 minRebaseTimeIntervalSec_, 215 | uint256 rebaseWindowOffsetSec_, 216 | uint256 rebaseWindowLengthSec_ 217 | ) external onlyOwner { 218 | require(minRebaseTimeIntervalSec_ > 0); 219 | require(rebaseWindowOffsetSec_ < minRebaseTimeIntervalSec_); 220 | 221 | minRebaseTimeIntervalSec = minRebaseTimeIntervalSec_; 222 | rebaseWindowOffsetSec = rebaseWindowOffsetSec_; 223 | rebaseWindowLengthSec = rebaseWindowLengthSec_; 224 | } 225 | 226 | /** 227 | * @notice A multi-chain AMPL interface method. The Ampleforth monetary policy contract 228 | * on the base-chain and XC-AmpleController contracts on the satellite-chains 229 | * implement this method. It atomically returns two values: 230 | * what the current contract believes to be, 231 | * the globalAmpleforthEpoch and globalAMPLSupply. 232 | * @return globalAmpleforthEpoch The current epoch number. 233 | * @return globalAMPLSupply The total supply at the current epoch. 234 | */ 235 | function globalAmpleforthEpochAndAMPLSupply() external view returns (uint256, uint256) { 236 | return (epoch, uFrags.totalSupply()); 237 | } 238 | 239 | /** 240 | * @dev ZOS upgradable contract initialization method. 241 | * It is called at the time of contract creation to invoke parent class initializers and 242 | * initialize the contract's state variables. 243 | */ 244 | function initialize( 245 | address owner_, 246 | IUFragments uFrags_, 247 | uint256 baseCpi_ 248 | ) public initializer { 249 | Ownable.initialize(owner_); 250 | 251 | // deviationThreshold = 0.05e18 = 5e16 252 | deviationThreshold = 5 * 10**(DECIMALS - 2); 253 | 254 | rebaseFunctionGrowth = int256(3 * (10**DECIMALS)); 255 | rebaseFunctionUpperPercentage = int256(10 * (10**(DECIMALS - 2))); // 0.1 256 | rebaseFunctionLowerPercentage = int256((-10) * int256(10**(DECIMALS - 2))); // -0.1 257 | 258 | minRebaseTimeIntervalSec = 1 days; 259 | rebaseWindowOffsetSec = 7200; // 2AM UTC 260 | rebaseWindowLengthSec = 20 minutes; 261 | 262 | lastRebaseTimestampSec = 0; 263 | epoch = 0; 264 | 265 | uFrags = uFrags_; 266 | baseCpi = baseCpi_; 267 | } 268 | 269 | /** 270 | * @return If the latest block timestamp is within the rebase time window it, returns true. 271 | * Otherwise, returns false. 272 | */ 273 | function inRebaseWindow() public view returns (bool) { 274 | return (block.timestamp.mod(minRebaseTimeIntervalSec) >= rebaseWindowOffsetSec && 275 | block.timestamp.mod(minRebaseTimeIntervalSec) < 276 | (rebaseWindowOffsetSec.add(rebaseWindowLengthSec))); 277 | } 278 | 279 | /** 280 | * Computes the percentage of supply to be added or removed: 281 | * Using the function in https://github.com/ampleforth/AIPs/blob/master/AIPs/aip-5.md 282 | * @param normalizedRate value of rate/targetRate in DECIMALS decimal fixed point number 283 | * @return The percentage of supply to be added or removed. 284 | */ 285 | function computeRebasePercentage( 286 | int256 normalizedRate, 287 | int256 lower, 288 | int256 upper, 289 | int256 growth 290 | ) public pure returns (int256) { 291 | int256 delta; 292 | 293 | delta = (normalizedRate.sub(ONE)); 294 | 295 | // Compute: (Upper-Lower)/(1-(Upper/Lower)/2^(Growth*delta))) + Lower 296 | 297 | int256 exponent = growth.mul(delta).div(ONE); 298 | // Cap exponent to guarantee it is not too big for twoPower 299 | if (exponent > ONE.mul(100)) { 300 | exponent = ONE.mul(100); 301 | } 302 | if (exponent < ONE.mul(-100)) { 303 | exponent = ONE.mul(-100); 304 | } 305 | 306 | int256 pow = SafeMathInt.twoPower(exponent, ONE); // 2^(Growth*Delta) 307 | if (pow == 0) { 308 | return lower; 309 | } 310 | int256 numerator = upper.sub(lower); //(Upper-Lower) 311 | int256 intermediate = upper.mul(ONE).div(lower); 312 | intermediate = intermediate.mul(ONE).div(pow); 313 | int256 denominator = ONE.sub(intermediate); // (1-(Upper/Lower)/2^(Growth*delta))) 314 | 315 | int256 rebasePercentage = (numerator.mul(ONE).div(denominator)).add(lower); 316 | return rebasePercentage; 317 | } 318 | 319 | /** 320 | * @return Computes the total supply adjustment in response to the exchange rate 321 | * and the targetRate. 322 | */ 323 | function computeSupplyDelta(uint256 rate, uint256 targetRate) internal view returns (int256) { 324 | if (withinDeviationThreshold(rate, targetRate)) { 325 | return 0; 326 | } 327 | int256 targetRateSigned = targetRate.toInt256Safe(); 328 | int256 normalizedRate = rate.toInt256Safe().mul(ONE).div(targetRateSigned); 329 | int256 rebasePercentage = computeRebasePercentage( 330 | normalizedRate, 331 | rebaseFunctionLowerPercentage, 332 | rebaseFunctionUpperPercentage, 333 | rebaseFunctionGrowth 334 | ); 335 | 336 | return uFrags.totalSupply().toInt256Safe().mul(rebasePercentage).div(ONE); 337 | } 338 | 339 | /** 340 | * @param rate The current exchange rate, an 18 decimal fixed point number. 341 | * @param targetRate The target exchange rate, an 18 decimal fixed point number. 342 | * @return If the rate is within the deviation threshold from the target rate, returns true. 343 | * Otherwise, returns false. 344 | */ 345 | function withinDeviationThreshold(uint256 rate, uint256 targetRate) 346 | internal 347 | view 348 | returns (bool) 349 | { 350 | uint256 absoluteDeviationThreshold = targetRate.mul(deviationThreshold).div(10**DECIMALS); 351 | 352 | return 353 | (rate >= targetRate && rate.sub(targetRate) < absoluteDeviationThreshold) || 354 | (rate < targetRate && targetRate.sub(rate) < absoluteDeviationThreshold); 355 | } 356 | 357 | /** 358 | * To maintain abi backward compatibility 359 | */ 360 | function rebaseLag() public pure returns (uint256) { 361 | return 1; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /contracts/UFragments.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.4; 3 | 4 | import "./_external/SafeMath.sol"; 5 | import "./_external/Ownable.sol"; 6 | import "./_external/ERC20Detailed.sol"; 7 | 8 | import "./lib/SafeMathInt.sol"; 9 | 10 | /** 11 | * @title uFragments ERC20 token 12 | * @dev This is part of an implementation of the uFragments Ideal Money protocol. 13 | * uFragments is a normal ERC20 token, but its supply can be adjusted by splitting and 14 | * combining tokens proportionally across all wallets. 15 | * 16 | * uFragment balances are internally represented with a hidden denomination, 'gons'. 17 | * We support splitting the currency in expansion and combining the currency on contraction by 18 | * changing the exchange rate between the hidden 'gons' and the public 'fragments'. 19 | */ 20 | contract UFragments is ERC20Detailed, Ownable { 21 | // PLEASE READ BEFORE CHANGING ANY ACCOUNTING OR MATH 22 | // Anytime there is division, there is a risk of numerical instability from rounding errors. In 23 | // order to minimize this risk, we adhere to the following guidelines: 24 | // 1) The conversion rate adopted is the number of gons that equals 1 fragment. 25 | // The inverse rate must not be used--TOTAL_GONS is always the numerator and _totalSupply is 26 | // always the denominator. (i.e. If you want to convert gons to fragments instead of 27 | // multiplying by the inverse rate, you should divide by the normal rate) 28 | // 2) Gon balances converted into Fragments are always rounded down (truncated). 29 | // 30 | // We make the following guarantees: 31 | // - If address 'A' transfers x Fragments to address 'B'. A's resulting external balance will 32 | // be decreased by precisely x Fragments, and B's external balance will be precisely 33 | // increased by x Fragments. 34 | // 35 | // We do not guarantee that the sum of all balances equals the result of calling totalSupply(). 36 | // This is because, for any conversion function 'f()' that has non-zero rounding error, 37 | // f(x0) + f(x1) + ... + f(xn) is not always equal to f(x0 + x1 + ... xn). 38 | using SafeMath for uint256; 39 | using SafeMathInt for int256; 40 | 41 | event LogRebase(uint256 indexed epoch, uint256 totalSupply); 42 | event LogMonetaryPolicyUpdated(address monetaryPolicy); 43 | 44 | // Used for authentication 45 | address public monetaryPolicy; 46 | 47 | modifier onlyMonetaryPolicy() { 48 | require(msg.sender == monetaryPolicy); 49 | _; 50 | } 51 | 52 | bool private rebasePausedDeprecated; 53 | bool private tokenPausedDeprecated; 54 | 55 | modifier validRecipient(address to) { 56 | require(to != address(0x0)); 57 | require(to != address(this)); 58 | _; 59 | } 60 | 61 | uint256 private constant DECIMALS = 9; 62 | uint256 private constant MAX_UINT256 = type(uint256).max; 63 | uint256 private constant INITIAL_FRAGMENTS_SUPPLY = 50 * 10**6 * 10**DECIMALS; 64 | 65 | // TOTAL_GONS is a multiple of INITIAL_FRAGMENTS_SUPPLY so that _gonsPerFragment is an integer. 66 | // Use the highest value that fits in a uint256 for max granularity. 67 | uint256 private constant TOTAL_GONS = MAX_UINT256 - (MAX_UINT256 % INITIAL_FRAGMENTS_SUPPLY); 68 | 69 | // MAX_SUPPLY = maximum integer < (sqrt(4*TOTAL_GONS + 1) - 1) / 2 70 | uint256 private constant MAX_SUPPLY = type(uint128).max; // (2^128) - 1 71 | 72 | uint256 private _totalSupply; 73 | uint256 private _gonsPerFragment; 74 | mapping(address => uint256) private _gonBalances; 75 | 76 | // This is denominated in Fragments, because the gons-fragments conversion might change before 77 | // it's fully paid. 78 | mapping(address => mapping(address => uint256)) private _allowedFragments; 79 | 80 | // EIP-2612: permit – 712-signed approvals 81 | // https://eips.ethereum.org/EIPS/eip-2612 82 | string public constant EIP712_REVISION = "1"; 83 | bytes32 public constant EIP712_DOMAIN = 84 | keccak256( 85 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 86 | ); 87 | bytes32 public constant PERMIT_TYPEHASH = 88 | keccak256( 89 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" 90 | ); 91 | 92 | // EIP-2612: keeps track of number of permits per address 93 | mapping(address => uint256) private _nonces; 94 | 95 | /** 96 | * @param monetaryPolicy_ The address of the monetary policy contract to use for authentication. 97 | */ 98 | function setMonetaryPolicy(address monetaryPolicy_) external onlyOwner { 99 | monetaryPolicy = monetaryPolicy_; 100 | emit LogMonetaryPolicyUpdated(monetaryPolicy_); 101 | } 102 | 103 | /** 104 | * @dev Notifies Fragments contract about a new rebase cycle. 105 | * @param supplyDelta The number of new fragment tokens to add into circulation via expansion. 106 | * @return The total number of fragments after the supply adjustment. 107 | */ 108 | function rebase(uint256 epoch, int256 supplyDelta) 109 | external 110 | onlyMonetaryPolicy 111 | returns (uint256) 112 | { 113 | if (supplyDelta == 0) { 114 | emit LogRebase(epoch, _totalSupply); 115 | return _totalSupply; 116 | } 117 | 118 | if (supplyDelta < 0) { 119 | _totalSupply = _totalSupply.sub(uint256(supplyDelta.abs())); 120 | } else { 121 | _totalSupply = _totalSupply.add(uint256(supplyDelta)); 122 | } 123 | 124 | if (_totalSupply > MAX_SUPPLY) { 125 | _totalSupply = MAX_SUPPLY; 126 | } 127 | 128 | _gonsPerFragment = TOTAL_GONS.div(_totalSupply); 129 | 130 | // From this point forward, _gonsPerFragment is taken as the source of truth. 131 | // We recalculate a new _totalSupply to be in agreement with the _gonsPerFragment 132 | // conversion rate. 133 | // This means our applied supplyDelta can deviate from the requested supplyDelta, 134 | // but this deviation is guaranteed to be < (_totalSupply^2)/(TOTAL_GONS - _totalSupply). 135 | // 136 | // In the case of _totalSupply <= MAX_UINT128 (our current supply cap), this 137 | // deviation is guaranteed to be < 1, so we can omit this step. If the supply cap is 138 | // ever increased, it must be re-included. 139 | // _totalSupply = TOTAL_GONS.div(_gonsPerFragment) 140 | 141 | emit LogRebase(epoch, _totalSupply); 142 | return _totalSupply; 143 | } 144 | 145 | function initialize(address owner_) public override initializer { 146 | ERC20Detailed.initialize("Ampleforth", "AMPL", uint8(DECIMALS)); 147 | Ownable.initialize(owner_); 148 | 149 | rebasePausedDeprecated = false; 150 | tokenPausedDeprecated = false; 151 | 152 | _totalSupply = INITIAL_FRAGMENTS_SUPPLY; 153 | _gonBalances[owner_] = TOTAL_GONS; 154 | _gonsPerFragment = TOTAL_GONS.div(_totalSupply); 155 | 156 | emit Transfer(address(0x0), owner_, _totalSupply); 157 | } 158 | 159 | /** 160 | * @return The total number of fragments. 161 | */ 162 | function totalSupply() external view override returns (uint256) { 163 | return _totalSupply; 164 | } 165 | 166 | /** 167 | * @param who The address to query. 168 | * @return The balance of the specified address. 169 | */ 170 | function balanceOf(address who) external view override returns (uint256) { 171 | return _gonBalances[who].div(_gonsPerFragment); 172 | } 173 | 174 | /** 175 | * @param who The address to query. 176 | * @return The gon balance of the specified address. 177 | */ 178 | function scaledBalanceOf(address who) external view returns (uint256) { 179 | return _gonBalances[who]; 180 | } 181 | 182 | /** 183 | * @return the total number of gons. 184 | */ 185 | function scaledTotalSupply() external pure returns (uint256) { 186 | return TOTAL_GONS; 187 | } 188 | 189 | /** 190 | * @return The number of successful permits by the specified address. 191 | */ 192 | function nonces(address who) public view returns (uint256) { 193 | return _nonces[who]; 194 | } 195 | 196 | /** 197 | * @return The computed DOMAIN_SEPARATOR to be used off-chain services 198 | * which implement EIP-712. 199 | * https://eips.ethereum.org/EIPS/eip-2612 200 | */ 201 | function DOMAIN_SEPARATOR() public view returns (bytes32) { 202 | uint256 chainId; 203 | assembly { 204 | chainId := chainid() 205 | } 206 | return 207 | keccak256( 208 | abi.encode( 209 | EIP712_DOMAIN, 210 | keccak256(bytes(name())), 211 | keccak256(bytes(EIP712_REVISION)), 212 | chainId, 213 | address(this) 214 | ) 215 | ); 216 | } 217 | 218 | /** 219 | * @dev Transfer tokens to a specified address. 220 | * @param to The address to transfer to. 221 | * @param value The amount to be transferred. 222 | * @return True on success, false otherwise. 223 | */ 224 | function transfer(address to, uint256 value) 225 | external 226 | override 227 | validRecipient(to) 228 | returns (bool) 229 | { 230 | uint256 gonValue = value.mul(_gonsPerFragment); 231 | 232 | _gonBalances[msg.sender] = _gonBalances[msg.sender].sub(gonValue); 233 | _gonBalances[to] = _gonBalances[to].add(gonValue); 234 | 235 | emit Transfer(msg.sender, to, value); 236 | return true; 237 | } 238 | 239 | /** 240 | * @dev Transfer all of the sender's wallet balance to a specified address. 241 | * @param to The address to transfer to. 242 | * @return True on success, false otherwise. 243 | */ 244 | function transferAll(address to) external validRecipient(to) returns (bool) { 245 | uint256 gonValue = _gonBalances[msg.sender]; 246 | uint256 value = gonValue.div(_gonsPerFragment); 247 | 248 | delete _gonBalances[msg.sender]; 249 | _gonBalances[to] = _gonBalances[to].add(gonValue); 250 | 251 | emit Transfer(msg.sender, to, value); 252 | return true; 253 | } 254 | 255 | /** 256 | * @dev Function to check the amount of tokens that an owner has allowed to a spender. 257 | * @param owner_ The address which owns the funds. 258 | * @param spender The address which will spend the funds. 259 | * @return The number of tokens still available for the spender. 260 | */ 261 | function allowance(address owner_, address spender) external view override returns (uint256) { 262 | return _allowedFragments[owner_][spender]; 263 | } 264 | 265 | /** 266 | * @dev Transfer tokens from one address to another. 267 | * @param from The address you want to send tokens from. 268 | * @param to The address you want to transfer to. 269 | * @param value The amount of tokens to be transferred. 270 | */ 271 | function transferFrom( 272 | address from, 273 | address to, 274 | uint256 value 275 | ) external override validRecipient(to) returns (bool) { 276 | _allowedFragments[from][msg.sender] = _allowedFragments[from][msg.sender].sub(value); 277 | 278 | uint256 gonValue = value.mul(_gonsPerFragment); 279 | _gonBalances[from] = _gonBalances[from].sub(gonValue); 280 | _gonBalances[to] = _gonBalances[to].add(gonValue); 281 | 282 | emit Transfer(from, to, value); 283 | return true; 284 | } 285 | 286 | /** 287 | * @dev Transfer all balance tokens from one address to another. 288 | * @param from The address you want to send tokens from. 289 | * @param to The address you want to transfer to. 290 | */ 291 | function transferAllFrom(address from, address to) external validRecipient(to) returns (bool) { 292 | uint256 gonValue = _gonBalances[from]; 293 | uint256 value = gonValue.div(_gonsPerFragment); 294 | 295 | _allowedFragments[from][msg.sender] = _allowedFragments[from][msg.sender].sub(value); 296 | 297 | delete _gonBalances[from]; 298 | _gonBalances[to] = _gonBalances[to].add(gonValue); 299 | 300 | emit Transfer(from, to, value); 301 | return true; 302 | } 303 | 304 | /** 305 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of 306 | * msg.sender. This method is included for ERC20 compatibility. 307 | * increaseAllowance and decreaseAllowance should be used instead. 308 | * Changing an allowance with this method brings the risk that someone may transfer both 309 | * the old and the new allowance - if they are both greater than zero - if a transfer 310 | * transaction is mined before the later approve() call is mined. 311 | * 312 | * @param spender The address which will spend the funds. 313 | * @param value The amount of tokens to be spent. 314 | */ 315 | function approve(address spender, uint256 value) external override returns (bool) { 316 | _allowedFragments[msg.sender][spender] = value; 317 | 318 | emit Approval(msg.sender, spender, value); 319 | return true; 320 | } 321 | 322 | /** 323 | * @dev Increase the amount of tokens that an owner has allowed to a spender. 324 | * This method should be used instead of approve() to avoid the double approval vulnerability 325 | * described above. 326 | * @param spender The address which will spend the funds. 327 | * @param addedValue The amount of tokens to increase the allowance by. 328 | */ 329 | function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { 330 | _allowedFragments[msg.sender][spender] = _allowedFragments[msg.sender][spender].add( 331 | addedValue 332 | ); 333 | 334 | emit Approval(msg.sender, spender, _allowedFragments[msg.sender][spender]); 335 | return true; 336 | } 337 | 338 | /** 339 | * @dev Decrease the amount of tokens that an owner has allowed to a spender. 340 | * 341 | * @param spender The address which will spend the funds. 342 | * @param subtractedValue The amount of tokens to decrease the allowance by. 343 | */ 344 | function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) { 345 | uint256 oldValue = _allowedFragments[msg.sender][spender]; 346 | _allowedFragments[msg.sender][spender] = (subtractedValue >= oldValue) 347 | ? 0 348 | : oldValue.sub(subtractedValue); 349 | 350 | emit Approval(msg.sender, spender, _allowedFragments[msg.sender][spender]); 351 | return true; 352 | } 353 | 354 | /** 355 | * @dev Allows for approvals to be made via secp256k1 signatures. 356 | * @param owner The owner of the funds 357 | * @param spender The spender 358 | * @param value The amount 359 | * @param deadline The deadline timestamp, type(uint256).max for max deadline 360 | * @param v Signature param 361 | * @param s Signature param 362 | * @param r Signature param 363 | */ 364 | function permit( 365 | address owner, 366 | address spender, 367 | uint256 value, 368 | uint256 deadline, 369 | uint8 v, 370 | bytes32 r, 371 | bytes32 s 372 | ) public { 373 | require(block.timestamp <= deadline); 374 | 375 | uint256 ownerNonce = _nonces[owner]; 376 | bytes32 permitDataDigest = keccak256( 377 | abi.encode(PERMIT_TYPEHASH, owner, spender, value, ownerNonce, deadline) 378 | ); 379 | bytes32 digest = keccak256( 380 | abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), permitDataDigest) 381 | ); 382 | 383 | require(owner == ecrecover(digest, v, r, s)); 384 | 385 | _nonces[owner] = ownerNonce.add(1); 386 | 387 | _allowedFragments[owner][spender] = value; 388 | emit Approval(owner, spender, value); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /test/unit/UFragments.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from 'hardhat' 2 | import { Contract, Signer, BigNumber } from 'ethers' 3 | import { expect } from 'chai' 4 | 5 | const toUFrgDenomination = (ample: string): BigNumber => 6 | ethers.utils.parseUnits(ample, DECIMALS) 7 | 8 | const DECIMALS = 9 9 | const INITIAL_SUPPLY = ethers.utils.parseUnits('50', 6 + DECIMALS) 10 | const MAX_UINT256 = ethers.BigNumber.from(2).pow(256).sub(1) 11 | const MAX_INT256 = ethers.BigNumber.from(2).pow(255).sub(1) 12 | const TOTAL_GONS = MAX_UINT256.sub(MAX_UINT256.mod(INITIAL_SUPPLY)) 13 | 14 | const transferAmount = toUFrgDenomination('10') 15 | const unitTokenAmount = toUFrgDenomination('1') 16 | 17 | let accounts: Signer[], 18 | deployer: Signer, 19 | uFragments: Contract, 20 | initialSupply: BigNumber 21 | 22 | async function setupContracts() { 23 | // prepare signers 24 | accounts = await ethers.getSigners() 25 | deployer = accounts[0] 26 | // deploy upgradable token 27 | const factory = await ethers.getContractFactory('UFragments') 28 | uFragments = await upgrades.deployProxy( 29 | factory, 30 | [await deployer.getAddress()], 31 | { 32 | initializer: 'initialize(address)', 33 | }, 34 | ) 35 | // fetch initial supply 36 | initialSupply = await uFragments.totalSupply() 37 | } 38 | 39 | describe('UFragments', () => { 40 | before('setup UFragments contract', setupContracts) 41 | 42 | it('should reject any ether sent to it', async function () { 43 | const user = accounts[1] 44 | await expect(user.sendTransaction({ to: uFragments.address, value: 1 })).to 45 | .be.reverted 46 | }) 47 | }) 48 | 49 | describe('UFragments:Initialization', () => { 50 | before('setup UFragments contract', setupContracts) 51 | 52 | it('should transfer 50M uFragments to the deployer', async function () { 53 | expect(await uFragments.balanceOf(await deployer.getAddress())).to.eq( 54 | INITIAL_SUPPLY, 55 | ) 56 | }) 57 | 58 | it('should set the totalSupply to 50M', async function () { 59 | expect(await uFragments.totalSupply()).to.eq(INITIAL_SUPPLY) 60 | }) 61 | 62 | it('should set the owner', async function () { 63 | expect(await uFragments.owner()).to.eq(await deployer.getAddress()) 64 | }) 65 | 66 | it('should set detailed ERC20 parameters', async function () { 67 | expect(await uFragments.name()).to.eq('Ampleforth') 68 | expect(await uFragments.symbol()).to.eq('AMPL') 69 | expect(await uFragments.decimals()).to.eq(DECIMALS) 70 | }) 71 | }) 72 | 73 | describe('UFragments:setMonetaryPolicy', async () => { 74 | let policy: Signer, policyAddress: string 75 | 76 | before('setup UFragments contract', async () => { 77 | await setupContracts() 78 | policy = accounts[1] 79 | policyAddress = await policy.getAddress() 80 | }) 81 | 82 | it('should set reference to policy contract', async function () { 83 | await expect(uFragments.connect(deployer).setMonetaryPolicy(policyAddress)) 84 | .to.emit(uFragments, 'LogMonetaryPolicyUpdated') 85 | .withArgs(policyAddress) 86 | expect(await uFragments.monetaryPolicy()).to.eq(policyAddress) 87 | }) 88 | }) 89 | 90 | describe('UFragments:setMonetaryPolicy:accessControl', async () => { 91 | let policy: Signer, policyAddress: string 92 | 93 | before('setup UFragments contract', async () => { 94 | await setupContracts() 95 | policy = accounts[1] 96 | policyAddress = await policy.getAddress() 97 | }) 98 | 99 | it('should be callable by owner', async function () { 100 | await expect(uFragments.connect(deployer).setMonetaryPolicy(policyAddress)) 101 | .to.not.be.reverted 102 | }) 103 | }) 104 | 105 | describe('UFragments:setMonetaryPolicy:accessControl', async () => { 106 | let policy: Signer, policyAddress: string, user: Signer 107 | 108 | before('setup UFragments contract', async () => { 109 | await setupContracts() 110 | policy = accounts[1] 111 | user = accounts[2] 112 | policyAddress = await policy.getAddress() 113 | }) 114 | 115 | it('should NOT be callable by non-owner', async function () { 116 | await expect(uFragments.connect(user).setMonetaryPolicy(policyAddress)).to 117 | .be.reverted 118 | }) 119 | }) 120 | 121 | describe('UFragments:Rebase:accessControl', async () => { 122 | let user: Signer, userAddress: string 123 | 124 | before('setup UFragments contract', async function () { 125 | await setupContracts() 126 | user = accounts[1] 127 | userAddress = await user.getAddress() 128 | await uFragments.connect(deployer).setMonetaryPolicy(userAddress) 129 | }) 130 | 131 | it('should be callable by monetary policy', async function () { 132 | await expect(uFragments.connect(user).rebase(1, transferAmount)).to.not.be 133 | .reverted 134 | }) 135 | 136 | it('should not be callable by others', async function () { 137 | await expect(uFragments.connect(deployer).rebase(1, transferAmount)).to.be 138 | .reverted 139 | }) 140 | }) 141 | 142 | describe('UFragments:Rebase:Expansion', async () => { 143 | // Rebase +5M (10%), with starting balances A:750 and B:250. 144 | let A: Signer, B: Signer, policy: Signer 145 | const rebaseAmt = INITIAL_SUPPLY.div(10) 146 | 147 | before('setup UFragments contract', async function () { 148 | await setupContracts() 149 | A = accounts[2] 150 | B = accounts[3] 151 | policy = accounts[1] 152 | await uFragments 153 | .connect(deployer) 154 | .setMonetaryPolicy(await policy.getAddress()) 155 | await uFragments 156 | .connect(deployer) 157 | .transfer(await A.getAddress(), toUFrgDenomination('750')) 158 | await uFragments 159 | .connect(deployer) 160 | .transfer(await B.getAddress(), toUFrgDenomination('250')) 161 | 162 | expect(await uFragments.totalSupply()).to.eq(INITIAL_SUPPLY) 163 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 164 | toUFrgDenomination('750'), 165 | ) 166 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 167 | toUFrgDenomination('250'), 168 | ) 169 | 170 | expect(await uFragments.scaledTotalSupply()).to.eq(TOTAL_GONS) 171 | expect(await uFragments.scaledBalanceOf(await A.getAddress())).to.eq( 172 | '1736881338559742931353564775130318617799049769984608460591863250000000000', 173 | ) 174 | expect(await uFragments.scaledBalanceOf(await B.getAddress())).to.eq( 175 | '578960446186580977117854925043439539266349923328202820197287750000000000', 176 | ) 177 | }) 178 | 179 | it('should emit Rebase', async function () { 180 | await expect(uFragments.connect(policy).rebase(1, rebaseAmt)) 181 | .to.emit(uFragments, 'LogRebase') 182 | .withArgs(1, initialSupply.add(rebaseAmt)) 183 | }) 184 | 185 | it('should increase the totalSupply', async function () { 186 | expect(await uFragments.totalSupply()).to.eq(initialSupply.add(rebaseAmt)) 187 | }) 188 | 189 | it('should NOT CHANGE the scaledTotalSupply', async function () { 190 | expect(await uFragments.scaledTotalSupply()).to.eq(TOTAL_GONS) 191 | }) 192 | 193 | it('should increase individual balances', async function () { 194 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 195 | toUFrgDenomination('825'), 196 | ) 197 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 198 | toUFrgDenomination('275'), 199 | ) 200 | }) 201 | 202 | it('should NOT CHANGE the individual scaled balances', async function () { 203 | expect(await uFragments.scaledBalanceOf(await A.getAddress())).to.eq( 204 | '1736881338559742931353564775130318617799049769984608460591863250000000000', 205 | ) 206 | expect(await uFragments.scaledBalanceOf(await B.getAddress())).to.eq( 207 | '578960446186580977117854925043439539266349923328202820197287750000000000', 208 | ) 209 | }) 210 | 211 | it('should return the new supply', async function () { 212 | const returnVal = await uFragments 213 | .connect(policy) 214 | .callStatic.rebase(2, rebaseAmt) 215 | await uFragments.connect(policy).rebase(2, rebaseAmt) 216 | expect(await uFragments.totalSupply()).to.eq(returnVal) 217 | }) 218 | }) 219 | 220 | describe('UFragments:Rebase:Expansion', async function () { 221 | let policy: Signer 222 | const MAX_SUPPLY = ethers.BigNumber.from(2).pow(128).sub(1) 223 | 224 | describe('when totalSupply is less than MAX_SUPPLY and expands beyond', function () { 225 | before('setup UFragments contract', async function () { 226 | await setupContracts() 227 | policy = accounts[1] 228 | await uFragments 229 | .connect(deployer) 230 | .setMonetaryPolicy(await policy.getAddress()) 231 | const totalSupply = await uFragments.totalSupply.call() 232 | await uFragments 233 | .connect(policy) 234 | .rebase(1, MAX_SUPPLY.sub(totalSupply).sub(toUFrgDenomination('1'))) 235 | }) 236 | 237 | it('should emit Rebase', async function () { 238 | await expect( 239 | uFragments.connect(policy).rebase(2, toUFrgDenomination('2')), 240 | ) 241 | .to.emit(uFragments, 'LogRebase') 242 | .withArgs(2, MAX_SUPPLY) 243 | }) 244 | 245 | it('should increase the totalSupply to MAX_SUPPLY', async function () { 246 | expect(await uFragments.totalSupply()).to.eq(MAX_SUPPLY) 247 | }) 248 | }) 249 | 250 | describe('when totalSupply is MAX_SUPPLY and expands', function () { 251 | before(async function () { 252 | expect(await uFragments.totalSupply()).to.eq(MAX_SUPPLY) 253 | }) 254 | 255 | it('should emit Rebase', async function () { 256 | await expect( 257 | uFragments.connect(policy).rebase(3, toUFrgDenomination('2')), 258 | ) 259 | .to.emit(uFragments, 'LogRebase') 260 | .withArgs(3, MAX_SUPPLY) 261 | }) 262 | 263 | it('should NOT change the totalSupply', async function () { 264 | expect(await uFragments.totalSupply()).to.eq(MAX_SUPPLY) 265 | }) 266 | }) 267 | }) 268 | 269 | describe('UFragments:Rebase:NoChange', function () { 270 | // Rebase (0%), with starting balances A:750 and B:250. 271 | let A: Signer, B: Signer, policy: Signer 272 | 273 | before('setup UFragments contract', async function () { 274 | await setupContracts() 275 | A = accounts[2] 276 | B = accounts[3] 277 | policy = accounts[1] 278 | await uFragments 279 | .connect(deployer) 280 | .setMonetaryPolicy(await policy.getAddress()) 281 | await uFragments 282 | .connect(deployer) 283 | .transfer(await A.getAddress(), toUFrgDenomination('750')) 284 | await uFragments 285 | .connect(deployer) 286 | .transfer(await B.getAddress(), toUFrgDenomination('250')) 287 | 288 | expect(await uFragments.totalSupply()).to.eq(INITIAL_SUPPLY) 289 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 290 | toUFrgDenomination('750'), 291 | ) 292 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 293 | toUFrgDenomination('250'), 294 | ) 295 | 296 | expect(await uFragments.scaledTotalSupply()).to.eq(TOTAL_GONS) 297 | expect(await uFragments.scaledBalanceOf(await A.getAddress())).to.eq( 298 | '1736881338559742931353564775130318617799049769984608460591863250000000000', 299 | ) 300 | expect(await uFragments.scaledBalanceOf(await B.getAddress())).to.eq( 301 | '578960446186580977117854925043439539266349923328202820197287750000000000', 302 | ) 303 | }) 304 | 305 | it('should emit Rebase', async function () { 306 | await expect(uFragments.connect(policy).rebase(1, 0)) 307 | .to.emit(uFragments, 'LogRebase') 308 | .withArgs(1, initialSupply) 309 | }) 310 | 311 | it('should NOT CHANGE the totalSupply', async function () { 312 | expect(await uFragments.totalSupply()).to.eq(initialSupply) 313 | }) 314 | 315 | it('should NOT CHANGE the scaledTotalSupply', async function () { 316 | expect(await uFragments.scaledTotalSupply()).to.eq(TOTAL_GONS) 317 | }) 318 | 319 | it('should NOT CHANGE individual balances', async function () { 320 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 321 | toUFrgDenomination('750'), 322 | ) 323 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 324 | toUFrgDenomination('250'), 325 | ) 326 | }) 327 | 328 | it('should NOT CHANGE the individual scaled balances', async function () { 329 | expect(await uFragments.scaledBalanceOf(await A.getAddress())).to.eq( 330 | '1736881338559742931353564775130318617799049769984608460591863250000000000', 331 | ) 332 | expect(await uFragments.scaledBalanceOf(await B.getAddress())).to.eq( 333 | '578960446186580977117854925043439539266349923328202820197287750000000000', 334 | ) 335 | }) 336 | }) 337 | 338 | describe('UFragments:Rebase:Contraction', function () { 339 | // Rebase -5M (-10%), with starting balances A:750 and B:250. 340 | let A: Signer, B: Signer, policy: Signer 341 | const rebaseAmt = INITIAL_SUPPLY.div(10) 342 | 343 | before('setup UFragments contract', async function () { 344 | await setupContracts() 345 | A = accounts[2] 346 | B = accounts[3] 347 | policy = accounts[1] 348 | await uFragments 349 | .connect(deployer) 350 | .setMonetaryPolicy(await policy.getAddress()) 351 | await uFragments 352 | .connect(deployer) 353 | .transfer(await A.getAddress(), toUFrgDenomination('750')) 354 | await uFragments 355 | .connect(deployer) 356 | .transfer(await B.getAddress(), toUFrgDenomination('250')) 357 | 358 | expect(await uFragments.totalSupply()).to.eq(INITIAL_SUPPLY) 359 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 360 | toUFrgDenomination('750'), 361 | ) 362 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 363 | toUFrgDenomination('250'), 364 | ) 365 | 366 | expect(await uFragments.scaledTotalSupply()).to.eq(TOTAL_GONS) 367 | expect(await uFragments.scaledBalanceOf(await A.getAddress())).to.eq( 368 | '1736881338559742931353564775130318617799049769984608460591863250000000000', 369 | ) 370 | expect(await uFragments.scaledBalanceOf(await B.getAddress())).to.eq( 371 | '578960446186580977117854925043439539266349923328202820197287750000000000', 372 | ) 373 | }) 374 | 375 | it('should emit Rebase', async function () { 376 | await expect(uFragments.connect(policy).rebase(1, -rebaseAmt)) 377 | .to.emit(uFragments, 'LogRebase') 378 | .withArgs(1, initialSupply.sub(rebaseAmt)) 379 | }) 380 | 381 | it('should decrease the totalSupply', async function () { 382 | expect(await uFragments.totalSupply()).to.eq(initialSupply.sub(rebaseAmt)) 383 | }) 384 | 385 | it('should NOT. CHANGE the scaledTotalSupply', async function () { 386 | expect(await uFragments.scaledTotalSupply()).to.eq(TOTAL_GONS) 387 | }) 388 | 389 | it('should decrease individual balances', async function () { 390 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 391 | toUFrgDenomination('675'), 392 | ) 393 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 394 | toUFrgDenomination('225'), 395 | ) 396 | }) 397 | 398 | it('should NOT CHANGE the individual scaled balances', async function () { 399 | expect(await uFragments.scaledBalanceOf(await A.getAddress())).to.eq( 400 | '1736881338559742931353564775130318617799049769984608460591863250000000000', 401 | ) 402 | expect(await uFragments.scaledBalanceOf(await B.getAddress())).to.eq( 403 | '578960446186580977117854925043439539266349923328202820197287750000000000', 404 | ) 405 | }) 406 | }) 407 | 408 | describe('UFragments:Transfer', function () { 409 | let A: Signer, B: Signer, C: Signer 410 | 411 | before('setup UFragments contract', async () => { 412 | await setupContracts() 413 | A = accounts[2] 414 | B = accounts[3] 415 | C = accounts[4] 416 | }) 417 | 418 | describe('deployer transfers 12 to A', function () { 419 | it('should have correct balances', async function () { 420 | const deployerBefore = await uFragments.balanceOf( 421 | await deployer.getAddress(), 422 | ) 423 | await uFragments 424 | .connect(deployer) 425 | .transfer(await A.getAddress(), toUFrgDenomination('12')) 426 | expect(await uFragments.balanceOf(await deployer.getAddress())).to.eq( 427 | deployerBefore.sub(toUFrgDenomination('12')), 428 | ) 429 | expect(await uFragments.balanceOf(await A.getAddress())).to.eq( 430 | toUFrgDenomination('12'), 431 | ) 432 | }) 433 | }) 434 | 435 | describe('deployer transfers 15 to B', async function () { 436 | it('should have balances [973,15]', async function () { 437 | const deployerBefore = await uFragments.balanceOf( 438 | await deployer.getAddress(), 439 | ) 440 | await uFragments 441 | .connect(deployer) 442 | .transfer(await B.getAddress(), toUFrgDenomination('15')) 443 | expect(await uFragments.balanceOf(await deployer.getAddress())).to.eq( 444 | deployerBefore.sub(toUFrgDenomination('15')), 445 | ) 446 | expect(await uFragments.balanceOf(await B.getAddress())).to.eq( 447 | toUFrgDenomination('15'), 448 | ) 449 | }) 450 | }) 451 | 452 | describe('deployer transfers the rest to C', async function () { 453 | it('should have balances [0,973]', async function () { 454 | const deployerBefore = await uFragments.balanceOf( 455 | await deployer.getAddress(), 456 | ) 457 | await uFragments 458 | .connect(deployer) 459 | .transfer(await C.getAddress(), deployerBefore) 460 | expect(await uFragments.balanceOf(await deployer.getAddress())).to.eq(0) 461 | expect(await uFragments.balanceOf(await C.getAddress())).to.eq( 462 | deployerBefore, 463 | ) 464 | }) 465 | }) 466 | 467 | describe('when the recipient address is the contract address', function () { 468 | it('reverts on transfer', async function () { 469 | await expect( 470 | uFragments.connect(A).transfer(uFragments.address, unitTokenAmount), 471 | ).to.be.reverted 472 | }) 473 | 474 | it('reverts on transferFrom', async function () { 475 | await expect( 476 | uFragments 477 | .connect(A) 478 | .transferFrom( 479 | await A.getAddress(), 480 | uFragments.address, 481 | unitTokenAmount, 482 | ), 483 | ).to.be.reverted 484 | }) 485 | }) 486 | 487 | describe('when the recipient is the zero address', function () { 488 | it('emits an approval event', async function () { 489 | await expect( 490 | uFragments 491 | .connect(A) 492 | .approve(ethers.constants.AddressZero, transferAmount), 493 | ) 494 | .to.emit(uFragments, 'Approval') 495 | .withArgs( 496 | await A.getAddress(), 497 | ethers.constants.AddressZero, 498 | transferAmount, 499 | ) 500 | }) 501 | 502 | it('transferFrom should fail', async function () { 503 | await expect( 504 | uFragments 505 | .connect(C) 506 | .transferFrom( 507 | await A.getAddress(), 508 | ethers.constants.AddressZero, 509 | unitTokenAmount, 510 | ), 511 | ).to.be.reverted 512 | }) 513 | }) 514 | }) 515 | -------------------------------------------------------------------------------- /test/unit/MedianOracle.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { BigNumber, Contract, ContractFactory, Signer } from 'ethers' 3 | import { increaseTime } from '../utils/utils' 4 | import { expect } from 'chai' 5 | 6 | let factory: ContractFactory 7 | let oracle: Contract 8 | let accounts: Signer[], 9 | deployer: Signer, 10 | A: Signer, 11 | B: Signer, 12 | C: Signer, 13 | D: Signer 14 | let payload: BigNumber 15 | let callerContract: Contract 16 | 17 | async function setupContractsAndAccounts() { 18 | accounts = await ethers.getSigners() 19 | deployer = accounts[0] 20 | A = accounts[1] 21 | B = accounts[2] 22 | C = accounts[3] 23 | D = accounts[4] 24 | factory = await ethers.getContractFactory('MedianOracle') 25 | oracle = await factory.deploy() 26 | await oracle.init(60, 10, 1) 27 | await oracle.deployed() 28 | } 29 | 30 | async function setupCallerContract() { 31 | let callerContractFactory = await ethers.getContractFactory( 32 | 'GetMedianOracleDataCallerContract', 33 | ) 34 | callerContract = await callerContractFactory.deploy() 35 | await callerContract.deployed() 36 | await callerContract.setOracle(oracle.address) 37 | } 38 | 39 | describe('MedianOracle:constructor', async function () { 40 | before(async function () { 41 | await setupContractsAndAccounts() 42 | }) 43 | 44 | it('should fail if a parameter is invalid', async function () { 45 | await expect(factory.deploy(60, 10, 0)).to.be.reverted 46 | await expect(factory.deploy(60 * 60 * 24 * 365 * 11, 10, 1)).to.be.reverted 47 | await expect(oracle.setReportExpirationTimeSec(60 * 60 * 24 * 365 * 11)).to 48 | .be.reverted 49 | }) 50 | }) 51 | 52 | describe('MedianOracle:providersSize', async function () { 53 | before(async function () { 54 | await setupContractsAndAccounts() 55 | }) 56 | 57 | it('should return the number of sources added to the whitelist', async function () { 58 | await oracle.addProvider(await A.getAddress()) 59 | await oracle.addProvider(await B.getAddress()) 60 | await oracle.addProvider(await C.getAddress()) 61 | expect(await oracle.providersSize()).to.eq(BigNumber.from(3)) 62 | }) 63 | }) 64 | 65 | describe('MedianOracle:addProvider', async function () { 66 | before(async function () { 67 | await setupContractsAndAccounts() 68 | expect(await oracle.providersSize()).to.eq(BigNumber.from(0)) 69 | }) 70 | 71 | it('should emit ProviderAdded message', async function () { 72 | await expect(oracle.addProvider(await A.getAddress())) 73 | .to.emit(oracle, 'ProviderAdded') 74 | .withArgs(await A.getAddress()) 75 | }) 76 | 77 | describe('when successful', async function () { 78 | it('should add source to the whitelist', async function () { 79 | expect(await oracle.providersSize()).to.eq(BigNumber.from(1)) 80 | }) 81 | it('should not add an existing source to the whitelist', async function () { 82 | await expect(oracle.addProvider(await A.getAddress())).to.be.reverted 83 | }) 84 | }) 85 | }) 86 | 87 | describe('MedianOracle:pushReport', async function () { 88 | beforeEach(async function () { 89 | await setupContractsAndAccounts() 90 | payload = BigNumber.from('1000000000000000000') 91 | }) 92 | it('should only push from authorized source', async function () { 93 | await expect(oracle.connect(A).pushReport(payload)).to.be.reverted 94 | }) 95 | it('should fail if reportDelaySec did not pass since the previous push', async function () { 96 | await oracle.addProvider(await A.getAddress()) 97 | await oracle.connect(A).pushReport(payload) 98 | await expect(oracle.connect(A).pushReport(payload)).to.be.reverted 99 | }) 100 | it('should emit ProviderReportPushed message', async function () { 101 | oracle.addProvider(await A.getAddress()) 102 | const tx = await oracle.connect(A).pushReport(payload) 103 | const txReceipt = await tx.wait() 104 | const txBlock = (await ethers.provider.getBlock(txReceipt.blockNumber)) 105 | .timestamp 106 | const txEvents = txReceipt.events?.filter((x: any) => { 107 | return x.event == 'ProviderReportPushed' 108 | }) 109 | expect(txEvents.length).to.equal(1) 110 | const event = txEvents[0] 111 | expect(event.args.length).to.equal(3) 112 | expect(event.args.provider).to.equal(await A.getAddress()) 113 | expect(event.args.payload).to.equal(payload) 114 | expect(event.args.timestamp).to.equal(txBlock) 115 | }) 116 | }) 117 | 118 | describe('MedianOracle:addProvider:accessControl', async function () { 119 | before(async function () { 120 | await setupContractsAndAccounts() 121 | }) 122 | 123 | it('should be callable by owner', async function () { 124 | await oracle.addProvider(await A.getAddress()) 125 | }) 126 | 127 | it('should NOT be callable by non-owner', async function () { 128 | await expect(oracle.connect(B).addProvider(A)).to.be.reverted 129 | }) 130 | }) 131 | 132 | describe('MedianOracle:removeProvider', async function () { 133 | describe('when source is part of the whitelist', () => { 134 | before(async function () { 135 | payload = BigNumber.from('1000000000000000000') 136 | await setupContractsAndAccounts() 137 | await oracle.addProvider(await A.getAddress()) 138 | await oracle.addProvider(await B.getAddress()) 139 | await oracle.addProvider(await C.getAddress()) 140 | await oracle.addProvider(await D.getAddress()) 141 | expect(await oracle.providersSize()).to.eq(BigNumber.from(4)) 142 | }) 143 | it('should emit ProviderRemoved message', async function () { 144 | expect(await oracle.removeProvider(await B.getAddress())) 145 | .to.emit(oracle, 'ProviderRemoved') 146 | .withArgs(await B.getAddress()) 147 | }) 148 | it('should remove source from the whitelist', async function () { 149 | expect(await oracle.providersSize()).to.eq(BigNumber.from(3)) 150 | await expect(oracle.connect(B).pushReport(payload)).to.be.reverted 151 | await oracle.connect(D).pushReport(payload) 152 | }) 153 | }) 154 | }) 155 | 156 | describe('MedianOracle:removeProvider', async function () { 157 | beforeEach(async function () { 158 | await setupContractsAndAccounts() 159 | await oracle.addProvider(await A.getAddress()) 160 | await oracle.addProvider(await B.getAddress()) 161 | await oracle.addProvider(await C.getAddress()) 162 | await oracle.addProvider(await D.getAddress()) 163 | expect(await oracle.providersSize()).to.eq(BigNumber.from(4)) 164 | }) 165 | it('Remove last element', async function () { 166 | await oracle.removeProvider(await D.getAddress()) 167 | expect(await oracle.providersSize()).to.eq(BigNumber.from(3)) 168 | expect(await oracle.providers(0)).to.eq(await A.getAddress()) 169 | expect(await oracle.providers(1)).to.eq(await B.getAddress()) 170 | expect(await oracle.providers(2)).to.eq(await C.getAddress()) 171 | }) 172 | 173 | it('Remove middle element', async function () { 174 | await oracle.removeProvider(await B.getAddress()) 175 | expect(await oracle.providersSize()).to.eq(BigNumber.from(3)) 176 | expect(await oracle.providers(0)).to.eq(await A.getAddress()) 177 | expect(await oracle.providers(1)).to.eq(await D.getAddress()) 178 | expect(await oracle.providers(2)).to.eq(await C.getAddress()) 179 | }) 180 | 181 | it('Remove only element', async function () { 182 | await oracle.removeProvider(await A.getAddress()) 183 | await oracle.removeProvider(await B.getAddress()) 184 | await oracle.removeProvider(await C.getAddress()) 185 | expect(await oracle.providersSize()).to.eq(BigNumber.from(1)) 186 | expect(await oracle.providers(0)).to.eq(await D.getAddress()) 187 | await oracle.removeProvider(await D.getAddress()) 188 | expect(await oracle.providersSize()).to.eq(BigNumber.from(0)) 189 | }) 190 | }) 191 | 192 | describe('MedianOracle:removeProvider', async function () { 193 | it('when provider is NOT part of the whitelist', async function () { 194 | await setupContractsAndAccounts() 195 | await oracle.addProvider(await A.getAddress()) 196 | await oracle.addProvider(await B.getAddress()) 197 | expect(await oracle.providersSize()).to.eq(BigNumber.from(2)) 198 | await oracle.removeProvider(await C.getAddress()) 199 | expect(await oracle.providersSize()).to.eq(BigNumber.from(2)) 200 | expect(await oracle.providers(0)).to.eq(await A.getAddress()) 201 | expect(await oracle.providers(1)).to.eq(await B.getAddress()) 202 | }) 203 | }) 204 | 205 | describe('MedianOracle:removeProvider:accessControl', async function () { 206 | beforeEach(async function () { 207 | await setupContractsAndAccounts() 208 | await oracle.addProvider(await A.getAddress()) 209 | }) 210 | 211 | it('should be callable by owner', async function () { 212 | await oracle.removeProvider(await A.getAddress()) 213 | }) 214 | 215 | it('should NOT be callable by non-owner', async function () { 216 | await expect(oracle.connect(A).removeProvider(await A.getAddress())).to.be 217 | .reverted 218 | }) 219 | }) 220 | 221 | describe('MedianOracle:getData', async function () { 222 | before(async function () { 223 | await setupContractsAndAccounts() 224 | await setupCallerContract() 225 | 226 | await oracle.addProvider(await A.getAddress()) 227 | await oracle.addProvider(await B.getAddress()) 228 | await oracle.addProvider(await C.getAddress()) 229 | await oracle.addProvider(await D.getAddress()) 230 | 231 | await oracle.connect(D).pushReport(BigNumber.from('1000000000000000000')) 232 | await oracle.connect(B).pushReport(BigNumber.from('1041000000000000000')) 233 | await oracle.connect(A).pushReport(BigNumber.from('1053200000000000000')) 234 | await oracle.connect(C).pushReport(BigNumber.from('2041000000000000000')) 235 | 236 | await increaseTime(40) 237 | }) 238 | 239 | describe('when the reports are valid', function () { 240 | it('should calculate the combined market rate and volume', async function () { 241 | await expect(callerContract.getData()) 242 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 243 | .withArgs(BigNumber.from('1047100000000000000'), true) 244 | }) 245 | }) 246 | }) 247 | 248 | describe('MedianOracle:getData', async function () { 249 | describe('when one of reports has expired', function () { 250 | before(async function () { 251 | await setupContractsAndAccounts() 252 | await setupCallerContract() 253 | 254 | await oracle.addProvider(await A.getAddress()) 255 | await oracle.addProvider(await B.getAddress()) 256 | await oracle.addProvider(await C.getAddress()) 257 | await oracle.addProvider(await D.getAddress()) 258 | 259 | await oracle.setReportExpirationTimeSec(40) 260 | await oracle.connect(C).pushReport(BigNumber.from('2041000000000000000')) 261 | await increaseTime(41) 262 | 263 | await oracle.connect(B).pushReport(BigNumber.from('1041000000000000000')) 264 | await oracle.connect(D).pushReport(BigNumber.from('1000000000000000000')) 265 | await oracle.connect(A).pushReport(BigNumber.from('1053200000000000000')) 266 | await increaseTime(10) 267 | }) 268 | 269 | it('should emit ReportTimestampOutOfRange message', async function () { 270 | await expect(oracle.getData()) 271 | .to.emit(oracle, 'ReportTimestampOutOfRange') 272 | .withArgs(await C.getAddress()) 273 | }) 274 | it('should calculate the exchange rate', async function () { 275 | await expect(callerContract.getData()) 276 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 277 | .withArgs(BigNumber.from('1041000000000000000'), true) 278 | }) 279 | }) 280 | }) 281 | 282 | describe('MedianOracle:getData', async function () { 283 | describe('when one of the reports is too recent', function () { 284 | before(async function () { 285 | await setupContractsAndAccounts() 286 | await setupCallerContract() 287 | 288 | await oracle.addProvider(await A.getAddress()) 289 | await oracle.addProvider(await B.getAddress()) 290 | await oracle.addProvider(await C.getAddress()) 291 | await oracle.addProvider(await D.getAddress()) 292 | 293 | await oracle.connect(C).pushReport(BigNumber.from('2041000000000000000')) 294 | await oracle.connect(D).pushReport(BigNumber.from('1000000000000000000')) 295 | await oracle.connect(A).pushReport(BigNumber.from('1053200000000000000')) 296 | await increaseTime(10) 297 | await oracle.connect(B).pushReport(BigNumber.from('1041000000000000000')) 298 | }) 299 | 300 | it('should emit ReportTimestampOutOfRange message', async function () { 301 | await expect(oracle.getData()) 302 | .to.emit(oracle, 'ReportTimestampOutOfRange') 303 | .withArgs(await B.getAddress()) 304 | }) 305 | it('should calculate the exchange rate', async function () { 306 | await expect(callerContract.getData()) 307 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 308 | .withArgs(BigNumber.from('1053200000000000000'), true) 309 | }) 310 | }) 311 | }) 312 | 313 | describe('MedianOracle:getData', async function () { 314 | describe('when not enough providers are valid', function () { 315 | before(async function () { 316 | await setupContractsAndAccounts() 317 | await setupCallerContract() 318 | 319 | await oracle.addProvider(await A.getAddress()) 320 | await oracle.addProvider(await B.getAddress()) 321 | await oracle.addProvider(await C.getAddress()) 322 | await oracle.addProvider(await D.getAddress()) 323 | 324 | await expect(oracle.setMinimumProviders(0)).to.be.reverted 325 | await oracle.setMinimumProviders(4) 326 | 327 | await oracle.connect(C).pushReport(BigNumber.from('2041000000000000000')) 328 | await oracle.connect(D).pushReport(BigNumber.from('1000000000000000000')) 329 | await oracle.connect(A).pushReport(BigNumber.from('1053200000000000000')) 330 | await increaseTime(10) 331 | await oracle.connect(B).pushReport(BigNumber.from('1041000000000000000')) 332 | }) 333 | 334 | it('should emit ReportTimestampOutOfRange message', async function () { 335 | await expect(oracle.getData()) 336 | .to.emit(oracle, 'ReportTimestampOutOfRange') 337 | .withArgs(await B.getAddress()) 338 | }) 339 | it('should not have a valid result', async function () { 340 | await expect(callerContract.getData()) 341 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 342 | .withArgs(BigNumber.from(0), false) 343 | }) 344 | }) 345 | }) 346 | 347 | describe('MedianOracle:getData', async function () { 348 | describe('when all reports have expired', function () { 349 | before(async function () { 350 | await setupContractsAndAccounts() 351 | await setupCallerContract() 352 | 353 | await oracle.addProvider(await A.getAddress()) 354 | await oracle.addProvider(await B.getAddress()) 355 | 356 | await oracle.connect(A).pushReport(BigNumber.from('1053200000000000000')) 357 | await oracle.connect(B).pushReport(BigNumber.from('1041000000000000000')) 358 | 359 | await increaseTime(61) 360 | }) 361 | 362 | it('should emit 2 ReportTimestampOutOfRange messages', async function () { 363 | const tx = await oracle.getData() 364 | const txReceipt = await tx.wait() 365 | const txEvents = txReceipt.events?.filter((x: any) => { 366 | return x.event == 'ReportTimestampOutOfRange' 367 | }) 368 | expect(txEvents.length).to.equal(2) 369 | const eventA = txEvents[0] 370 | expect(eventA.args.provider).to.equal(await A.getAddress()) 371 | const eventB = txEvents[1] 372 | expect(eventB.args.provider).to.equal(await B.getAddress()) 373 | }) 374 | it('should return false and 0', async function () { 375 | await expect(callerContract.getData()) 376 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 377 | .withArgs(BigNumber.from(0), false) 378 | }) 379 | }) 380 | }) 381 | 382 | describe('MedianOracle:getData', async function () { 383 | before(async function () { 384 | await setupContractsAndAccounts() 385 | await setupCallerContract() 386 | 387 | await oracle.addProvider(await A.getAddress()) 388 | 389 | await oracle.connect(A).pushReport(BigNumber.from('1100000000000000000')) 390 | await increaseTime(61) 391 | await oracle.connect(A).pushReport(BigNumber.from('1200000000000000000')) 392 | }) 393 | 394 | describe('when recent is too recent and past is too old', function () { 395 | it('should emit ReportTimestampOutOfRange message', async function () { 396 | await expect(oracle.getData()) 397 | .to.emit(oracle, 'ReportTimestampOutOfRange') 398 | .withArgs(await A.getAddress()) 399 | }) 400 | it('should fail', async function () { 401 | await expect(callerContract.getData()) 402 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 403 | .withArgs(BigNumber.from(0), false) 404 | }) 405 | }) 406 | }) 407 | 408 | describe('MedianOracle:getData', async function () { 409 | before(async function () { 410 | await setupContractsAndAccounts() 411 | await setupCallerContract() 412 | await oracle.addProvider(await A.getAddress()) 413 | 414 | await oracle.connect(A).pushReport(BigNumber.from('1100000000000000000')) 415 | await increaseTime(10) 416 | await oracle.connect(A).pushReport(BigNumber.from('1200000000000000000')) 417 | await increaseTime(1) 418 | await oracle.setReportDelaySec(30) 419 | }) 420 | 421 | describe('when recent is too recent and past is too recent', function () { 422 | it('should emit ReportTimestampOutOfRange message', async function () { 423 | await expect(oracle.getData()) 424 | .to.emit(oracle, 'ReportTimestampOutOfRange') 425 | .withArgs(await A.getAddress()) 426 | }) 427 | it('should fail', async function () { 428 | await expect(callerContract.getData()) 429 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 430 | .withArgs(BigNumber.from(0), false) 431 | }) 432 | }) 433 | }) 434 | 435 | describe('MedianOracle:getData', async function () { 436 | before(async function () { 437 | await setupContractsAndAccounts() 438 | await setupCallerContract() 439 | 440 | await oracle.addProvider(await A.getAddress()) 441 | 442 | await oracle.connect(A).pushReport(BigNumber.from('1100000000000000000')) 443 | await increaseTime(30) 444 | await oracle.connect(A).pushReport(BigNumber.from('1200000000000000000')) 445 | await increaseTime(4) 446 | }) 447 | 448 | describe('when recent is too recent and past is valid', function () { 449 | it('should succeed', async function () { 450 | await expect(callerContract.getData()) 451 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 452 | .withArgs(BigNumber.from('1100000000000000000'), true) 453 | }) 454 | }) 455 | }) 456 | 457 | describe('MedianOracle:getData', async function () { 458 | before(async function () { 459 | await setupContractsAndAccounts() 460 | await setupCallerContract() 461 | 462 | await oracle.addProvider(await A.getAddress()) 463 | 464 | await oracle.connect(A).pushReport(BigNumber.from('1100000000000000000')) 465 | await increaseTime(30) 466 | await oracle.connect(A).pushReport(BigNumber.from('1200000000000000000')) 467 | await increaseTime(10) 468 | }) 469 | 470 | describe('when recent is not too recent nor too old', function () { 471 | it('should succeed', async function () { 472 | await expect(callerContract.getData()) 473 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 474 | .withArgs(BigNumber.from('1200000000000000000'), true) 475 | }) 476 | }) 477 | }) 478 | 479 | describe('MedianOracle:getData', async function () { 480 | before(async function () { 481 | await setupContractsAndAccounts() 482 | await setupCallerContract() 483 | 484 | await oracle.addProvider(await A.getAddress()) 485 | 486 | await oracle.connect(A).pushReport(BigNumber.from('1100000000000000000')) 487 | await increaseTime(30) 488 | await oracle.connect(A).pushReport(BigNumber.from('1200000000000000000')) 489 | await increaseTime(80) 490 | }) 491 | 492 | describe('when recent is not too recent but too old', function () { 493 | it('should fail', async function () { 494 | await expect(callerContract.getData()) 495 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 496 | .withArgs(BigNumber.from(0), false) 497 | }) 498 | }) 499 | }) 500 | 501 | describe('MedianOracle:PurgeReports', async function () { 502 | before(async function () { 503 | await setupContractsAndAccounts() 504 | await setupCallerContract() 505 | 506 | await oracle.addProvider(await A.getAddress()) 507 | 508 | await oracle.connect(A).pushReport(BigNumber.from('1100000000000000000')) 509 | await increaseTime(20) 510 | await oracle.connect(A).pushReport(BigNumber.from('1200000000000000000')) 511 | await increaseTime(20) 512 | await oracle.connect(A).purgeReports() 513 | }) 514 | 515 | it('data not available after purge', async function () { 516 | await expect(callerContract.getData()) 517 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 518 | .withArgs(BigNumber.from(0), false) 519 | }) 520 | it('data available after another report', async function () { 521 | await oracle.connect(A).pushReport(BigNumber.from('1300000000000000000')) 522 | await increaseTime(20) 523 | await expect(callerContract.getData()) 524 | .to.emit(callerContract, 'ReturnValueUInt256Bool') 525 | .withArgs(BigNumber.from('1300000000000000000'), true) 526 | }) 527 | it('cannot purge a non-whitelisted provider', async function () { 528 | await expect(oracle.connect(B).purgeReports()).to.be.reverted 529 | await oracle.connect(A).purgeReports() 530 | await oracle.removeProvider(await A.getAddress()) 531 | await expect(oracle.connect(A).purgeReports()).to.be.reverted 532 | }) 533 | }) 534 | -------------------------------------------------------------------------------- /test/unit/uFragments_erc20_behavior.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | Copyright (c) 2016 Smart Contract Solutions, Inc. 4 | Copyright (c) 2018 Fragments, Inc. 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | This file tests if the UFragments contract confirms to the ERC20 specification. 21 | These test cases are inspired from OpenZepplin's ERC20 unit test. 22 | https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/test/token/ERC20/ERC20.test.js 23 | */ 24 | 25 | import { ethers, upgrades, waffle } from 'hardhat' 26 | import { Contract, Signer, BigNumber } from 'ethers' 27 | import { TransactionResponse } from '@ethersproject/providers' 28 | import { expect } from 'chai' 29 | 30 | const toUFrgDenomination = (ample: string): BigNumber => 31 | ethers.utils.parseUnits(ample, DECIMALS) 32 | 33 | const DECIMALS = 9 34 | const INITIAL_SUPPLY = ethers.utils.parseUnits('50', 6 + DECIMALS) 35 | const transferAmount = toUFrgDenomination('10') 36 | const unitTokenAmount = toUFrgDenomination('1') 37 | const overdraftAmount = INITIAL_SUPPLY.add(unitTokenAmount) 38 | const overdraftAmountPlusOne = overdraftAmount.add(unitTokenAmount) 39 | const overdraftAmountMinusOne = overdraftAmount.sub(unitTokenAmount) 40 | const transferAmountPlusOne = transferAmount.add(unitTokenAmount) 41 | const transferAmountMinusOne = transferAmount.sub(unitTokenAmount) 42 | 43 | let token: Contract, owner: Signer, anotherAccount: Signer, recipient: Signer 44 | 45 | async function upgradeableToken() { 46 | const [owner, recipient, anotherAccount] = await ethers.getSigners() 47 | const factory = await ethers.getContractFactory('UFragments') 48 | const token = await upgrades.deployProxy( 49 | factory.connect(owner), 50 | [await owner.getAddress()], 51 | { 52 | initializer: 'initialize(address)', 53 | }, 54 | ) 55 | return { token, owner, recipient, anotherAccount } 56 | } 57 | 58 | describe('UFragments:ERC20', () => { 59 | before('setup UFragments contract', async function () { 60 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 61 | upgradeableToken, 62 | )) 63 | }) 64 | 65 | describe('totalSupply', function () { 66 | it('returns the total amount of tokens', async function () { 67 | expect(await token.totalSupply()).to.eq(INITIAL_SUPPLY) 68 | }) 69 | }) 70 | 71 | describe('balanceOf', function () { 72 | describe('when the requested account has no tokens', function () { 73 | it('returns zero', async function () { 74 | expect(await token.balanceOf(await anotherAccount.getAddress())).to.eq( 75 | 0, 76 | ) 77 | }) 78 | }) 79 | 80 | describe('when the requested account has some tokens', function () { 81 | it('returns the total amount of tokens', async function () { 82 | expect(await token.balanceOf(await owner.getAddress())).to.eq( 83 | INITIAL_SUPPLY, 84 | ) 85 | }) 86 | }) 87 | }) 88 | }) 89 | 90 | describe('UFragments:ERC20:transfer', () => { 91 | before('setup UFragments contract', async function () { 92 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 93 | upgradeableToken, 94 | )) 95 | }) 96 | 97 | describe('when the sender does NOT have enough balance', function () { 98 | it('reverts', async function () { 99 | await expect( 100 | token 101 | .connect(owner) 102 | .transfer(await recipient.getAddress(), overdraftAmount), 103 | ).to.be.reverted 104 | }) 105 | }) 106 | 107 | describe('when the sender has enough balance', function () { 108 | it('should emit a transfer event', async function () { 109 | await expect( 110 | token 111 | .connect(owner) 112 | .transfer(await recipient.getAddress(), transferAmount), 113 | ) 114 | .to.emit(token, 'Transfer') 115 | .withArgs( 116 | await owner.getAddress(), 117 | await recipient.getAddress(), 118 | transferAmount, 119 | ) 120 | }) 121 | 122 | it('should transfer the requested amount', async function () { 123 | const senderBalance = await token.balanceOf(await owner.getAddress()) 124 | const recipientBalance = await token.balanceOf( 125 | await recipient.getAddress(), 126 | ) 127 | const supply = await token.totalSupply() 128 | expect(supply.sub(transferAmount)).to.eq(senderBalance) 129 | expect(recipientBalance).to.eq(transferAmount) 130 | }) 131 | }) 132 | 133 | describe('when the recipient is the zero address', function () { 134 | it('should fail', async function () { 135 | await expect( 136 | token 137 | .connect(owner) 138 | .transfer(ethers.constants.AddressZero, transferAmount), 139 | ).to.be.reverted 140 | }) 141 | }) 142 | }) 143 | 144 | describe('UFragments:ERC20:transferFrom', () => { 145 | before('setup UFragments contract', async function () { 146 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 147 | upgradeableToken, 148 | )) 149 | }) 150 | 151 | describe('when the spender does NOT have enough approved balance', function () { 152 | describe('when the owner does NOT have enough balance', function () { 153 | it('reverts', async function () { 154 | await token 155 | .connect(owner) 156 | .approve(await anotherAccount.getAddress(), overdraftAmountMinusOne) 157 | await expect( 158 | token 159 | .connect(anotherAccount) 160 | .transferFrom( 161 | await owner.getAddress(), 162 | await recipient.getAddress(), 163 | overdraftAmount, 164 | ), 165 | ).to.be.reverted 166 | }) 167 | }) 168 | 169 | describe('when the owner has enough balance', function () { 170 | it('reverts', async function () { 171 | await token 172 | .connect(owner) 173 | .approve(await anotherAccount.getAddress(), transferAmountMinusOne) 174 | await expect( 175 | token 176 | .connect(anotherAccount) 177 | .transferFrom( 178 | await owner.getAddress(), 179 | await recipient.getAddress(), 180 | transferAmount, 181 | ), 182 | ).to.be.reverted 183 | }) 184 | }) 185 | }) 186 | 187 | describe('when the spender has enough approved balance', function () { 188 | describe('when the owner does NOT have enough balance', function () { 189 | it('should fail', async function () { 190 | await token 191 | .connect(owner) 192 | .approve(await anotherAccount.getAddress(), overdraftAmount) 193 | await expect( 194 | token 195 | .connect(anotherAccount) 196 | .transferFrom( 197 | await owner.getAddress(), 198 | await recipient.getAddress(), 199 | overdraftAmount, 200 | ), 201 | ).to.be.reverted 202 | }) 203 | }) 204 | 205 | describe('when the owner has enough balance', function () { 206 | let prevSenderBalance: BigNumber 207 | before(async function () { 208 | prevSenderBalance = await token.balanceOf(await owner.getAddress()) 209 | await token 210 | .connect(owner) 211 | .approve(await anotherAccount.getAddress(), transferAmount) 212 | }) 213 | 214 | it('emits a transfer event', async function () { 215 | await expect( 216 | token 217 | .connect(anotherAccount) 218 | .transferFrom( 219 | await owner.getAddress(), 220 | await recipient.getAddress(), 221 | transferAmount, 222 | ), 223 | ) 224 | .to.emit(token, 'Transfer') 225 | .withArgs( 226 | await owner.getAddress(), 227 | await recipient.getAddress(), 228 | transferAmount, 229 | ) 230 | }) 231 | 232 | it('transfers the requested amount', async function () { 233 | const senderBalance = await token.balanceOf(await owner.getAddress()) 234 | const recipientBalance = await token.balanceOf( 235 | await recipient.getAddress(), 236 | ) 237 | expect(prevSenderBalance.sub(transferAmount)).to.eq(senderBalance) 238 | expect(recipientBalance).to.eq(transferAmount) 239 | }) 240 | 241 | it('decreases the spender allowance', async function () { 242 | expect( 243 | await token.allowance( 244 | await owner.getAddress(), 245 | await anotherAccount.getAddress(), 246 | ), 247 | ).to.eq(0) 248 | }) 249 | }) 250 | }) 251 | }) 252 | 253 | describe('UFragments:ERC20:approve', () => { 254 | before('setup UFragments contract', async function () { 255 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 256 | upgradeableToken, 257 | )) 258 | }) 259 | 260 | describe('when the spender is NOT the zero address', function () { 261 | describe('when the sender has enough balance', function () { 262 | describe('when there was no approved amount before', function () { 263 | let r: Promise 264 | before(async function () { 265 | await token 266 | .connect(owner) 267 | .approve(await anotherAccount.getAddress(), 0) 268 | r = token 269 | .connect(owner) 270 | .approve(await anotherAccount.getAddress(), transferAmount) 271 | }) 272 | 273 | it('approves the requested amount', async function () { 274 | await r 275 | expect( 276 | await token.allowance( 277 | await owner.getAddress(), 278 | await anotherAccount.getAddress(), 279 | ), 280 | ).to.eq(transferAmount) 281 | }) 282 | 283 | it('emits an approval event', async function () { 284 | await expect(r) 285 | .to.emit(token, 'Approval') 286 | .withArgs( 287 | await owner.getAddress(), 288 | await anotherAccount.getAddress(), 289 | transferAmount, 290 | ) 291 | }) 292 | }) 293 | 294 | describe('when the spender had an approved amount', function () { 295 | let r: Promise 296 | before(async function () { 297 | await token 298 | .connect(owner) 299 | .approve(await anotherAccount.getAddress(), toUFrgDenomination('1')) 300 | r = token 301 | .connect(owner) 302 | .approve(await anotherAccount.getAddress(), transferAmount) 303 | }) 304 | 305 | it('approves the requested amount and replaces the previous one', async function () { 306 | await r 307 | expect( 308 | await token.allowance( 309 | await owner.getAddress(), 310 | await anotherAccount.getAddress(), 311 | ), 312 | ).to.eq(transferAmount) 313 | }) 314 | 315 | it('emits an approval event', async function () { 316 | await expect(r) 317 | .to.emit(token, 'Approval') 318 | .withArgs( 319 | await owner.getAddress(), 320 | await anotherAccount.getAddress(), 321 | transferAmount, 322 | ) 323 | }) 324 | }) 325 | }) 326 | 327 | describe('when the sender does not have enough balance', function () { 328 | describe('when there was no approved amount before', function () { 329 | let r: Promise 330 | before(async function () { 331 | await token 332 | .connect(owner) 333 | .approve(await anotherAccount.getAddress(), 0) 334 | r = token 335 | .connect(owner) 336 | .approve(await anotherAccount.getAddress(), overdraftAmount) 337 | }) 338 | 339 | it('approves the requested amount', async function () { 340 | await r 341 | expect( 342 | await token.allowance( 343 | await owner.getAddress(), 344 | await anotherAccount.getAddress(), 345 | ), 346 | ).to.eq(overdraftAmount) 347 | }) 348 | 349 | it('emits an approval event', async function () { 350 | await expect(r) 351 | .to.emit(token, 'Approval') 352 | .withArgs( 353 | await owner.getAddress(), 354 | await anotherAccount.getAddress(), 355 | overdraftAmount, 356 | ) 357 | }) 358 | }) 359 | 360 | describe('when the spender had an approved amount', function () { 361 | let r: Promise 362 | before(async function () { 363 | await token 364 | .connect(owner) 365 | .approve(await anotherAccount.getAddress(), toUFrgDenomination('1')) 366 | r = token 367 | .connect(owner) 368 | .approve(await anotherAccount.getAddress(), overdraftAmount) 369 | }) 370 | 371 | it('approves the requested amount', async function () { 372 | await r 373 | expect( 374 | await token.allowance( 375 | await owner.getAddress(), 376 | await anotherAccount.getAddress(), 377 | ), 378 | ).to.eq(overdraftAmount) 379 | }) 380 | 381 | it('emits an approval event', async function () { 382 | await expect(r) 383 | .to.emit(token, 'Approval') 384 | .withArgs( 385 | await owner.getAddress(), 386 | await anotherAccount.getAddress(), 387 | overdraftAmount, 388 | ) 389 | }) 390 | }) 391 | }) 392 | }) 393 | }) 394 | 395 | describe('UFragments:ERC20:increaseAllowance', () => { 396 | before('setup UFragments contract', async function () { 397 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 398 | upgradeableToken, 399 | )) 400 | }) 401 | 402 | describe('when the spender is NOT the zero address', function () { 403 | describe('when the sender has enough balance', function () { 404 | describe('when there was no approved amount before', function () { 405 | let r: Promise 406 | before(async function () { 407 | await token 408 | .connect(owner) 409 | .approve(await anotherAccount.getAddress(), 0) 410 | r = token 411 | .connect(owner) 412 | .increaseAllowance( 413 | await anotherAccount.getAddress(), 414 | transferAmount, 415 | ) 416 | }) 417 | it('approves the requested amount', async function () { 418 | await r 419 | expect( 420 | await token.allowance( 421 | await owner.getAddress(), 422 | await anotherAccount.getAddress(), 423 | ), 424 | ).to.eq(transferAmount) 425 | }) 426 | 427 | it('emits an approval event', async function () { 428 | await expect(r) 429 | .to.emit(token, 'Approval') 430 | .withArgs( 431 | await owner.getAddress(), 432 | await anotherAccount.getAddress(), 433 | transferAmount, 434 | ) 435 | }) 436 | }) 437 | 438 | describe('when the spender had an approved amount', function () { 439 | let r: Promise 440 | beforeEach(async function () { 441 | await token 442 | .connect(owner) 443 | .approve(await anotherAccount.getAddress(), unitTokenAmount) 444 | r = token 445 | .connect(owner) 446 | .increaseAllowance( 447 | await anotherAccount.getAddress(), 448 | transferAmount, 449 | ) 450 | }) 451 | 452 | it('increases the spender allowance adding the requested amount', async function () { 453 | await r 454 | expect( 455 | await token.allowance( 456 | await owner.getAddress(), 457 | await anotherAccount.getAddress(), 458 | ), 459 | ).to.eq(transferAmountPlusOne) 460 | }) 461 | 462 | it('emits an approval event', async function () { 463 | await expect(r) 464 | .to.emit(token, 'Approval') 465 | .withArgs( 466 | await owner.getAddress(), 467 | await anotherAccount.getAddress(), 468 | transferAmountPlusOne, 469 | ) 470 | }) 471 | }) 472 | }) 473 | 474 | describe('when the sender does not have enough balance', function () { 475 | describe('when there was no approved amount before', function () { 476 | let r: Promise 477 | before(async function () { 478 | await token 479 | .connect(owner) 480 | .approve(await anotherAccount.getAddress(), 0) 481 | r = token 482 | .connect(owner) 483 | .increaseAllowance( 484 | await anotherAccount.getAddress(), 485 | overdraftAmount, 486 | ) 487 | }) 488 | 489 | it('approves the requested amount', async function () { 490 | await r 491 | expect( 492 | await token.allowance( 493 | await owner.getAddress(), 494 | await anotherAccount.getAddress(), 495 | ), 496 | ).to.eq(overdraftAmount) 497 | }) 498 | 499 | it('emits an approval event', async function () { 500 | await expect(r) 501 | .to.emit(token, 'Approval') 502 | .withArgs( 503 | await owner.getAddress(), 504 | await anotherAccount.getAddress(), 505 | overdraftAmount, 506 | ) 507 | }) 508 | }) 509 | 510 | describe('when the spender had an approved amount', function () { 511 | let r: Promise 512 | beforeEach(async function () { 513 | await token 514 | .connect(owner) 515 | .approve(await anotherAccount.getAddress(), unitTokenAmount) 516 | r = token 517 | .connect(owner) 518 | .increaseAllowance( 519 | await anotherAccount.getAddress(), 520 | overdraftAmount, 521 | ) 522 | }) 523 | 524 | it('increases the spender allowance adding the requested amount', async function () { 525 | await r 526 | expect( 527 | await token.allowance( 528 | await owner.getAddress(), 529 | await anotherAccount.getAddress(), 530 | ), 531 | ).to.eq(overdraftAmountPlusOne) 532 | }) 533 | 534 | it('emits an approval event', async function () { 535 | await expect(r) 536 | .to.emit(token, 'Approval') 537 | .withArgs( 538 | await owner.getAddress(), 539 | await anotherAccount.getAddress(), 540 | overdraftAmountPlusOne, 541 | ) 542 | }) 543 | }) 544 | }) 545 | }) 546 | }) 547 | 548 | describe('UFragments:ERC20:decreaseAllowance', () => { 549 | before('setup UFragments contract', async function () { 550 | ;({ token, owner, recipient, anotherAccount } = await waffle.loadFixture( 551 | upgradeableToken, 552 | )) 553 | }) 554 | 555 | describe('when the spender is NOT the zero address', function () { 556 | describe('when the sender does NOT have enough balance', function () { 557 | describe('when there was no approved amount before', function () { 558 | let r: Promise 559 | before(async function () { 560 | r = token 561 | .connect(owner) 562 | .decreaseAllowance( 563 | await anotherAccount.getAddress(), 564 | overdraftAmount, 565 | ) 566 | }) 567 | 568 | it('keeps the allowance to zero', async function () { 569 | await r 570 | expect( 571 | await token.allowance( 572 | await owner.getAddress(), 573 | await anotherAccount.getAddress(), 574 | ), 575 | ).to.eq(0) 576 | }) 577 | 578 | it('emits an approval event', async function () { 579 | await expect(r) 580 | .to.emit(token, 'Approval') 581 | .withArgs( 582 | await owner.getAddress(), 583 | await anotherAccount.getAddress(), 584 | 0, 585 | ) 586 | }) 587 | }) 588 | 589 | describe('when the spender had an approved amount', function () { 590 | let r: Promise 591 | before(async function () { 592 | await token 593 | .connect(owner) 594 | .approve(await anotherAccount.getAddress(), overdraftAmountPlusOne) 595 | r = token 596 | .connect(owner) 597 | .decreaseAllowance( 598 | await anotherAccount.getAddress(), 599 | overdraftAmount, 600 | ) 601 | }) 602 | 603 | it('decreases the spender allowance subtracting the requested amount', async function () { 604 | await r 605 | expect( 606 | await token.allowance( 607 | await owner.getAddress(), 608 | await anotherAccount.getAddress(), 609 | ), 610 | ).to.eq(unitTokenAmount) 611 | }) 612 | 613 | it('emits an approval event', async function () { 614 | await expect(r) 615 | .to.emit(token, 'Approval') 616 | .withArgs( 617 | await owner.getAddress(), 618 | await anotherAccount.getAddress(), 619 | unitTokenAmount, 620 | ) 621 | }) 622 | }) 623 | }) 624 | 625 | describe('when the sender has enough balance', function () { 626 | describe('when there was no approved amount before', function () { 627 | let r: Promise 628 | before(async function () { 629 | await token 630 | .connect(owner) 631 | .approve(await anotherAccount.getAddress(), 0) 632 | r = token 633 | .connect(owner) 634 | .decreaseAllowance( 635 | await anotherAccount.getAddress(), 636 | transferAmount, 637 | ) 638 | }) 639 | 640 | it('keeps the allowance to zero', async function () { 641 | await r 642 | expect( 643 | await token.allowance( 644 | await owner.getAddress(), 645 | await anotherAccount.getAddress(), 646 | ), 647 | ).to.eq(0) 648 | }) 649 | 650 | it('emits an approval event', async function () { 651 | await expect(r) 652 | .to.emit(token, 'Approval') 653 | .withArgs( 654 | await owner.getAddress(), 655 | await anotherAccount.getAddress(), 656 | 0, 657 | ) 658 | }) 659 | }) 660 | 661 | describe('when the spender had an approved amount', function () { 662 | let r: Promise 663 | before(async function () { 664 | await token 665 | .connect(owner) 666 | .approve(await anotherAccount.getAddress(), transferAmountPlusOne) 667 | r = token 668 | .connect(owner) 669 | .decreaseAllowance( 670 | await anotherAccount.getAddress(), 671 | transferAmount, 672 | ) 673 | }) 674 | 675 | it('decreases the spender allowance subtracting the requested amount', async function () { 676 | await r 677 | expect( 678 | await token.allowance( 679 | await owner.getAddress(), 680 | await anotherAccount.getAddress(), 681 | ), 682 | ).to.eq(unitTokenAmount) 683 | }) 684 | 685 | it('emits an approval event', async function () { 686 | await expect(r) 687 | .to.emit(token, 'Approval') 688 | .withArgs( 689 | await owner.getAddress(), 690 | await anotherAccount.getAddress(), 691 | unitTokenAmount, 692 | ) 693 | }) 694 | }) 695 | }) 696 | }) 697 | }) 698 | --------------------------------------------------------------------------------