From 86037dcebc4b61ddf579ea32c76851b1a0e61b7f Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Sun, 24 Dec 2023 16:44:33 +0100 Subject: [PATCH] add tests for new token, renaming --- contracts/optimism/L1ERC20TokenBridge.sol | 4 +- contracts/optimism/L2ERC20TokenBridge.sol | 4 +- contracts/stubs/ERC20WrapableStub.sol | 2 +- contracts/stubs/TokenRateOracleStub.sol | 6 +- contracts/token/ERC20Rebasable.sol | 99 +- contracts/token/interfaces/IERC20Wrapable.sol | 2 +- .../token/interfaces/ITokenRateOracle.sol | 2 +- .../optimism.integration.test.ts | 4 +- .../bridging-rebase.integration.test.ts | 46 +- test/optimism/deposit-gas-estimation.test.ts | 2 +- test/token/ERC20Rebasable.unit.test.ts | 931 +++++++++++++++--- utils/optimism/deployment.ts | 5 +- utils/optimism/testing.ts | 16 +- 13 files changed, 921 insertions(+), 202 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 67009c86..1c39797e 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -71,7 +71,7 @@ contract L1ERC20TokenBridge is whenDepositsEnabled onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) - { + { if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } @@ -140,7 +140,7 @@ contract L1ERC20TokenBridge is if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ - rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), // replace by stETHPerToken + rate: IERC20Wrapable(l1TokenNonRebasable).stETHPerToken(), time: block.timestamp, data: data_ }); diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 724c2db9..36ae5c0b 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -110,8 +110,8 @@ contract L2ERC20TokenBridge is { if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); - ITokenRateOracle tokensRateOracle = ERC20Rebasable(l2TokenRebasable).tokensRateOracle(); - tokensRateOracle.updateRate(int256(depositData.rate), depositData.time); + ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2TokenRebasable).tokenRateOracle(); + tokenRateOracle.updateRate(int256(depositData.rate), depositData.time, 0); ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index f3c7f33a..9f48b24f 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -47,7 +47,7 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { return stETHAmount; } - function tokensPerStEth() external view returns (uint256) { + function stETHPerToken() external view returns (uint256) { return tokensRate; } } diff --git a/contracts/stubs/TokenRateOracleStub.sol b/contracts/stubs/TokenRateOracleStub.sol index 4be61315..40463af7 100644 --- a/contracts/stubs/TokenRateOracleStub.sol +++ b/contracts/stubs/TokenRateOracleStub.sol @@ -49,9 +49,9 @@ contract TokenRateOracleStub is ITokenRateOracle { return latestRoundDataAnswer; } - function updateRate(int256 rate, uint256 updatedAt) external { + function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external { // check timestamp not late as current one. - latestRoundDataAnswer = rate; - latestRoundDataUpdatedAt = updatedAt; + latestRoundDataAnswer = tokenRate_; + latestRoundDataUpdatedAt = rateL1Timestamp_; } } \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index c975689a..b7a32798 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -7,7 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; -import { console } from "hardhat/console.sol"; +// import { console } from "hardhat/console.sol"; /// @author kovalgek /// @notice Extends the ERC20Shared functionality @@ -23,24 +23,43 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { error ErrorNotEnoughAllowance(); error ErrorAccountIsZeroAddress(); error ErrorDecreasedAllowanceBelowZero(); + error ErrorNotBridge(); + /// @notice Bridge which can mint and burn tokens on L2. + address public immutable bridge; + + /// @notice Contract of non-rebasable token to wrap. IERC20 public immutable wrappedToken; - ITokenRateOracle public immutable tokensRateOracle; - /// @param wrappedToken_ address of the ERC20 token to wrap - /// @param tokensRateOracle_ address of oracle that returns tokens rate + /// @notice Oracle contract used to get token rate for wrapping/unwrapping tokens. + ITokenRateOracle public immutable tokenRateOracle; + + /// @inheritdoc IERC20 + mapping(address => mapping(address => uint256)) public allowance; + + /// @notice Basic unit representing the token holder's share in the total amount of ether controlled by the protocol. + mapping (address => uint256) private shares; + + /// @notice The total amount of shares in existence. + uint256 private totalShares; + /// @param name_ The name of the token /// @param symbol_ The symbol of the token /// @param decimals_ The decimals places of the token + /// @param wrappedToken_ address of the ERC20 token to wrap + /// @param tokenRateOracle_ address of oracle that returns tokens rate + /// @param bridge_ The bridge address which allowd to mint/burn tokens constructor( - address wrappedToken_, - address tokensRateOracle_, string memory name_, string memory symbol_, - uint8 decimals_ + uint8 decimals_, + address wrappedToken_, + address tokenRateOracle_, + address bridge_ ) ERC20Metadata(name_, symbol_, decimals_) { wrappedToken = IERC20(wrappedToken_); - tokensRateOracle = ITokenRateOracle(tokensRateOracle_); + tokenRateOracle = ITokenRateOracle(tokenRateOracle_); + bridge = bridge_; } /// @notice Sets the name and the symbol of the tokens if they both are empty @@ -51,8 +70,6 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { _setERC20MetadataSymbol(symbol_); } - /// ------------IERC20Wrapable------------ - /// @inheritdoc IERC20Wrapable function wrap(uint256 sharesAmount_) external returns (uint256) { if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); @@ -75,25 +92,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } - function tokensPerStEth() external pure returns (uint256) { - return 0; + /// @inheritdoc IERC20Wrapable + function stETHPerToken() external view returns (uint256) { + return uint256(tokenRateOracle.latestAnswer()); } // allow call only bridge - function mintShares(address account_, uint256 amount_) external returns (uint256) { + function mintShares(address account_, uint256 amount_) external onlyBridge returns (uint256) { return _mintShares(account_, amount_); } // allow call only bridge - function burnShares(address account_, uint256 amount_) external { + function burnShares(address account_, uint256 amount_) external onlyBridge { _burnShares(account_, amount_); } - - /// ------------ERC20------------ - - /// @inheritdoc IERC20 - mapping(address => mapping(address => uint256)) public allowance; + /// @dev Validates that sender of the transaction is the bridge + modifier onlyBridge() { + if (msg.sender != bridge) { + revert ErrorNotBridge(); + } + _; + } /// @inheritdoc IERC20 function totalSupply() external view returns (uint256) { @@ -212,35 +232,32 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { emit Approval(owner_, spender_, amount_); } - - /// ------------Shares------------ - // API - function sharesOf(address _account) external view returns (uint256) { - return _sharesOf(_account); + /// @notice Get shares amount of the provided account. + /// @param account_ provided account address. + /// @return amount of shares owned by `_account`. + function sharesOf(address account_) external view returns (uint256) { + return _sharesOf(account_); } + /// @return total amount of shares. function getTotalShares() external view returns (uint256) { return _getTotalShares(); } + /// @notice Get amount of tokens for a given amount of shares. + /// @param sharesAmount_ amount of shares. + /// @return amount of tokens for a given shares amount. function getTokensByShares(uint256 sharesAmount_) external view returns (uint256) { return _getTokensByShares(sharesAmount_); } + /// @notice Get amount of shares for a given amount of tokens. + /// @param tokenAmount_ provided tokens amount. + /// @return amount of shares for a given tokens amount. function getSharesByTokens(uint256 tokenAmount_) external view returns (uint256) { return _getSharesByTokens(tokenAmount_); } - function getTokensRateAndDecimal() external view returns (uint256, uint256) { - return _getTokensRateAndDecimal(); - } - - // private/internal - - mapping (address => uint256) private shares; - - uint256 private totalShares; - function _sharesOf(address account_) internal view returns (uint256) { return shares[account_]; } @@ -251,32 +268,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); - return (sharesAmount_ * (10 ** decimals)) / tokensRate; + return (sharesAmount_ * tokensRate) / (10 ** decimals); } function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) { (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); - return (tokenAmount_ * tokensRate) / (10 ** decimals); + return (tokenAmount_ * (10 ** decimals)) / tokensRate; } function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { - uint8 rateDecimals = tokensRateOracle.decimals(); - console.log("_getTokensRateAndDecimal1"); + uint8 rateDecimals = tokenRateOracle.decimals(); if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); - console.log("_getTokensRateAndDecimal2"); (, int256 answer , , uint256 updatedAt - ,) = tokensRateOracle.latestRoundData(); - console.log("_getTokensRateAndDecimal3"); + ,) = tokenRateOracle.latestRoundData(); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); if (answer <= 0) revert ErrorOracleAnswerIsNegative(); - console.log("_getTokensRateAndDecimal4"); return (uint256(answer), uint256(rateDecimals)); } @@ -290,6 +303,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { ) internal onlyNonZeroAccount(recipient_) returns (uint256) { totalShares = totalShares + amount_; shares[recipient_] = shares[recipient_] + amount_; + emit Transfer(address(0), recipient_, amount_); return totalShares; } @@ -304,6 +318,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { if (accountShares < amount_) revert ErrorNotEnoughBalance(); totalShares = totalShares - amount_; shares[account_] = accountShares - amount_; + emit Transfer(account_, address(0), amount_); return totalShares; } diff --git a/contracts/token/interfaces/IERC20Wrapable.sol b/contracts/token/interfaces/IERC20Wrapable.sol index de82ee27..0fb2d2dc 100644 --- a/contracts/token/interfaces/IERC20Wrapable.sol +++ b/contracts/token/interfaces/IERC20Wrapable.sol @@ -34,5 +34,5 @@ interface IERC20Wrapable { * @notice Get amount of wstETH for a one stETH * @return Amount of wstETH for a 1 stETH */ - function tokensPerStEth() external view returns (uint256); + function stETHPerToken() external view returns (uint256); } \ No newline at end of file diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/token/interfaces/ITokenRateOracle.sol index 648b346d..eb9aa8aa 100644 --- a/contracts/token/interfaces/ITokenRateOracle.sol +++ b/contracts/token/interfaces/ITokenRateOracle.sol @@ -26,5 +26,5 @@ interface ITokenRateOracle { function decimals() external view returns (uint8); /// @notice Updates token rate. - function updateRate(int256 rate, uint256 rateL1Timestamp) external; + function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external; } \ No newline at end of file diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 2a8949f4..6cd18db8 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -219,7 +219,7 @@ async function ctxFactory() { "TTR" ); - const tokensRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy(); const optAddresses = optimism.addresses(networkName); @@ -243,7 +243,7 @@ async function ctxFactory() { .erc20TokenBridgeDeployScript( l1Token.address, l1TokenRebasable.address, - tokensRateOracleStub.address, + tokenRateOracleStub.address, { deployer: l1Deployer, admins: { diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index e0822643..cda397c2 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -79,12 +79,12 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -111,8 +111,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockNumber = await l1Provider.getBlockNumber(); const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToSend = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ @@ -165,16 +165,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l2Provider } = ctx; - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); const blockNumber = await l2Provider.getBlockNumber(); const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToReceive = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = ctx.accounts; @@ -206,8 +206,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); - const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); - assert.equalBN(tokensPerStEth, tokensRate); + const [,tokensRate,,updatedAt,] = await tokenRateOracle.latestRoundData(); + assert.equalBN(stETHPerToken, tokensRate); assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ @@ -237,15 +237,15 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); - await tokensRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); + await tokenRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -272,8 +272,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockNumber = await l1Provider.getBlockNumber(); const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToSend = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ @@ -326,19 +326,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l2Provider } = ctx; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; - const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); - const tokensPerStEth = await l1Token.tokensPerStEth(); + const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).div(2); + const stETHPerToken = await l1Token.stETHPerToken(); const blockNumber = await l2Provider.getBlockNumber(); const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToReceive = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = @@ -371,8 +371,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); - const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); - assert.equalBN(tokensPerStEth, tokensRate); + const [,tokensRate,,updatedAt,] = await tokenRateOracle.latestRoundData(); + assert.equalBN(stETHPerToken, tokensRate); assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ @@ -403,7 +403,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2ERC20TokenBridge } = ctx; - const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).mul(2); + const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).div(2); const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( tokenHolderA.address diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts index c6fc8db7..5a14cda6 100644 --- a/test/optimism/deposit-gas-estimation.test.ts +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -79,7 +79,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); await l1TokenRebasable .connect(tokenHolderA.l1Signer) diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 2d49abc6..ac87c9d5 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -6,7 +6,6 @@ import { wei } from "../../utils/wei"; import { ERC20Stub__factory, ERC20Rebasable__factory, TokenRateOracleStub__factory, OssifiableProxy__factory } from "../../typechain"; import { BigNumber } from "ethers"; - unit("ERC20Rebasable", ctxFactory) .test("wrappedToken", async (ctx) => { @@ -14,9 +13,9 @@ unit("ERC20Rebasable", ctxFactory) assert.equal(await rebasableProxied.wrappedToken(), wrappedTokenStub.address) }) - .test("tokensRateOracle", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; - assert.equal(await rebasableProxied.tokensRateOracle(), tokensRateOracleStub.address) + .test("tokenRateOracle", async (ctx) => { + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + assert.equal(await rebasableProxied.tokenRateOracle(), tokenRateOracleStub.address) }) .test("name()", async (ctx) => @@ -27,235 +26,935 @@ unit("ERC20Rebasable", ctxFactory) assert.equal(await ctx.contracts.rebasableProxied.symbol(), ctx.constants.symbol) ) + .test("initialize() :: name already set", async (ctx) => { + const { deployer, owner } = ctx.accounts; + + // deploy new implementation + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + "name", + "", + 10, + wrappedTokenStub.address, + tokenRateOracleStub.address, + owner.address + ); + await assert.revertsWith( + rebasableTokenImpl.initialize("New Name", ""), + "ErrorNameAlreadySet()" + ); + }) + + .test("initialize() :: symbol already set", async (ctx) => { + const { deployer, owner } = ctx.accounts; + + // deploy new implementation + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + "", + "symbol", + 10, + wrappedTokenStub.address, + tokenRateOracleStub.address, + owner.address + ); + await assert.revertsWith( + rebasableTokenImpl.initialize("", "New Symbol"), + "ErrorSymbolAlreadySet()" + ); + }) + .test("decimals", async (ctx) => - assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimals) + assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimalsToSet) ) + .test("totalShares", async (ctx) => { + const { premintShares } = ctx.constants; + assert.equalBN(await ctx.contracts.rebasableProxied.getTotalShares(), premintShares); + }) + .test("wrap(0)", async (ctx) => { const { rebasableProxied } = ctx.contracts; - await assert.revertsWith(rebasableProxied.wrap(0), "ErrorZeroSharesWrap()"); + const { user1 } = ctx.accounts; + await assert.revertsWith(rebasableProxied.connect(user1).wrap(0), "ErrorZeroSharesWrap()"); }) .test("unwrap(0)", async (ctx) => { const { rebasableProxied } = ctx.contracts; - await assert.revertsWith(rebasableProxied.unwrap(0), "ErrorZeroTokensUnwrap()"); + const { user1 } = ctx.accounts; + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(0), "ErrorZeroTokensUnwrap()"); }) .test("wrap() positive scenario", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub, wrappedTokenStub } = ctx.contracts; + + const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; + const { rate, decimals, premintShares } = ctx.constants; + + const totalSupply = rate.mul(premintShares).div(decimals); - await tokensRateOracleStub.setDecimals(5); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); // user1 - assert.equalBN(await rebasableProxied.callStatic.wrap(100), 83); - const tx = await rebasableProxied.wrap(100); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); - assert.equalBN(await rebasableProxied.getTotalShares(), 100); - assert.equalBN(await rebasableProxied.sharesOf(user1.address), 100); + const user1Shares = wei`100 ether`; + const user1Tokens = rate.mul(user1Shares).div(decimals); + + assert.equalBN(await rebasableProxied.connect(user1).callStatic.wrap(user1Shares), user1Tokens); + const tx = await rebasableProxied.connect(user1).wrap(user1Shares); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); assert.equal(await wrappedTokenStub.transferFromAddress(), user1.address); assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); - assert.equalBN(await wrappedTokenStub.transferFromAmount(), 100); + assert.equalBN(await wrappedTokenStub.transferFromAmount(), user1Shares); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens)); // user2 - assert.equalBN(await rebasableProxied.connect(user2).callStatic.wrap(50), 41); - const tx2 = await rebasableProxied.connect(user2).wrap(50); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - assert.equalBN(await rebasableProxied.getTotalShares(), 150); - assert.equalBN(await rebasableProxied.sharesOf(user2.address), 50); + const user2Shares = wei`50 ether`; + const user2Tokens = rate.mul(user2Shares).div(decimals); + assert.equalBN(await rebasableProxied.connect(user2).callStatic.wrap(user2Shares), user2Tokens); + const tx2 = await rebasableProxied.connect(user2).wrap(user2Shares); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); assert.equal(await wrappedTokenStub.transferFromAddress(), user2.address); assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); - assert.equalBN(await wrappedTokenStub.transferFromAmount(), 50); + assert.equalBN(await wrappedTokenStub.transferFromAmount(), user2Shares); // common state changes - assert.equalBN(await rebasableProxied.totalSupply(), 125); + assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); }) .test("wrap() with wrong oracle decimals", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; - - await tokensRateOracleStub.setDecimals(0); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.wrap(23), "ErrorInvalidRateDecimals(0)"); + await tokenRateOracleStub.setDecimals(0); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorInvalidRateDecimals(0)"); - await tokensRateOracleStub.setDecimals(19); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); - - await assert.revertsWith(rebasableProxied.wrap(23), "ErrorInvalidRateDecimals(19)"); + await tokenRateOracleStub.setDecimals(19); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorInvalidRateDecimals(19)"); }) .test("wrap() with wrong oracle update time", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; - - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(0); + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.wrap(5), "ErrorWrongOracleUpdateTime()"); + await tokenRateOracleStub.setUpdatedAt(0); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(5), "ErrorWrongOracleUpdateTime()"); }) .test("wrap() with wrong oracle answer", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(0); - await tokensRateOracleStub.setUpdatedAt(10); - - await assert.revertsWith(rebasableProxied.wrap(21), "ErrorOracleAnswerIsNegative()"); + await tokenRateOracleStub.setLatestRoundDataAnswer(0); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNegative()"); }) - .test("unwrap() positive scenario", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub, wrappedTokenStub } = ctx.contracts; + const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; + const { rate, decimals, premintShares } = ctx.constants; - await tokensRateOracleStub.setDecimals(7); - await tokensRateOracleStub.setLatestRoundDataAnswer(14000000); - await tokensRateOracleStub.setUpdatedAt(14000); + const totalSupply = BigNumber.from(rate).mul(premintShares).div(decimals); + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); // user1 - const tx0 = await rebasableProxied.wrap(4500); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + const user1SharesToWrap = wei`100 ether`; + const user1SharesToUnwrap = wei`59 ether`; + const user1TokensToUnwrap = rate.mul(user1SharesToUnwrap).div(decimals); - assert.equalBN(await rebasableProxied.callStatic.unwrap(59), 82); - const tx = await rebasableProxied.unwrap(59); + const user1Shares = BigNumber.from(user1SharesToWrap).sub(user1SharesToUnwrap); + const user1Tokens = BigNumber.from(rate).mul(user1Shares).div(decimals); - assert.equalBN(await rebasableProxied.getTotalShares(), 4418); - assert.equalBN(await rebasableProxied.sharesOf(user1.address), 4418); + const tx0 = await rebasableProxied.connect(user1).wrap(user1SharesToWrap); + assert.equalBN(await rebasableProxied.connect(user1).callStatic.unwrap(user1TokensToUnwrap), user1SharesToUnwrap); + const tx = await rebasableProxied.connect(user1).unwrap(user1TokensToUnwrap); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); assert.equal(await wrappedTokenStub.transferTo(), user1.address); - assert.equalBN(await wrappedTokenStub.transferAmount(), 82); + assert.equalBN(await wrappedTokenStub.transferAmount(), user1SharesToUnwrap); - // // user2 - await rebasableProxied.connect(user2).wrap(200); + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens)); + + // user2 + const user2SharesToWrap = wei`145 ether`; + const user2SharesToUnwrap = wei`14 ether`; + const user2TokensToUnwrap = rate.mul(user2SharesToUnwrap).div(decimals); - assert.equalBN(await rebasableProxied.connect(user2).callStatic.unwrap(50), 70); - const tx2 = await rebasableProxied.connect(user2).unwrap(50); + const user2Shares = BigNumber.from(user2SharesToWrap).sub(user2SharesToUnwrap); + const user2Tokens = BigNumber.from(rate).mul(user2Shares).div(decimals); - assert.equalBN(await rebasableProxied.getTotalShares(), 4548); - assert.equalBN(await rebasableProxied.sharesOf(user2.address), 130); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + await rebasableProxied.connect(user2).wrap(user2SharesToWrap); + assert.equalBN(await rebasableProxied.connect(user2).callStatic.unwrap(user2TokensToUnwrap), user2SharesToUnwrap); + const tx2 = await rebasableProxied.connect(user2).unwrap(user2TokensToUnwrap); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); assert.equal(await wrappedTokenStub.transferTo(), user2.address); - assert.equalBN(await wrappedTokenStub.transferAmount(), 70); + assert.equalBN(await wrappedTokenStub.transferAmount(), user2SharesToUnwrap); // common state changes - assert.equalBN(await rebasableProxied.totalSupply(), 3248); + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); }) .test("unwrap() with wrong oracle decimals", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + await rebasableProxied.connect(user1).wrap(wei`2 ether`); + + await tokenRateOracleStub.setDecimals(0); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorInvalidRateDecimals(0)"); + + await tokenRateOracleStub.setDecimals(19); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorInvalidRateDecimals(19)"); + }) + + .test("unwrap() with wrong oracle update time", async (ctx) => { - await rebasableProxied.wrap(100); - await tokensRateOracleStub.setDecimals(0); + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.unwrap(23), "ErrorInvalidRateDecimals(0)"); + await rebasableProxied.connect(user1).wrap(wei`6 ether`); + await tokenRateOracleStub.setUpdatedAt(0); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`1 ether`), "ErrorWrongOracleUpdateTime()"); + }) - await tokensRateOracleStub.setDecimals(19); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + .test("unwrap() when no balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.unwrap(23), "ErrorInvalidRateDecimals(19)"); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`4 ether`), "ErrorNotEnoughBalance()"); }) - .test("unwrap() with wrong oracle update time", async (ctx) => { + .test("mintShares() positive scenario", async (ctx) => { + + const { rebasableProxied } = ctx.contracts; + const {user1, user2, owner } = ctx.accounts; + const { rate, decimals, premintShares, premintTokens } = ctx.constants; + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); + + // user1 + const user1SharesToMint = wei`44 ether`; + const user1TokensMinted = rate.mul(user1SharesToMint).div(decimals); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + assert.equalBN(await rebasableProxied.connect(owner).callStatic.mintShares(user1.address, user1SharesToMint), premintShares.add(user1SharesToMint)); + const tx0 = await rebasableProxied.connect(owner).mintShares(user1.address, user1SharesToMint); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1TokensMinted); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1SharesToMint)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted)); + + // // user2 + const user2SharesToMint = wei`75 ether`; + const user2TokensMinted = rate.mul(user2SharesToMint).div(decimals); - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(300); + assert.equalBN( + await rebasableProxied.connect(owner).callStatic.mintShares(user2.address, user2SharesToMint), + premintShares.add(user1SharesToMint).add(user2SharesToMint) + ); + const tx1 = await rebasableProxied.connect(owner).mintShares(user2.address, user2SharesToMint); - await rebasableProxied.wrap(100); - await tokensRateOracleStub.setUpdatedAt(0); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); - await assert.revertsWith(rebasableProxied.unwrap(5), "ErrorWrongOracleUpdateTime()"); + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1SharesToMint).add(user2SharesToMint)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted).add(user2TokensMinted)); }) - .test("unwrap() when no balance", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + .test("burnShares() positive scenario", async (ctx) => { + + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const {user1, user2, owner } = ctx.accounts; + const { rate, decimals, premintShares, premintTokens } = ctx.constants; - await tokensRateOracleStub.setDecimals(8); - await tokensRateOracleStub.setLatestRoundDataAnswer(12000000); - await tokensRateOracleStub.setUpdatedAt(1000); + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); - await assert.revertsWith(rebasableProxied.unwrap(10), "ErrorNotEnoughBalance()"); + // user1 + const user1SharesToMint = wei`12 ether`; + const user1TokensMinted = rate.mul(user1SharesToMint).div(decimals); + + const user1SharesToBurn = wei`4 ether`; + const user1TokensBurned = rate.mul(user1SharesToBurn).div(decimals); + + const user1Shares = BigNumber.from(user1SharesToMint).sub(user1SharesToBurn); + const user1Tokens = user1TokensMinted.sub(user1TokensBurned); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + await rebasableProxied.connect(owner).mintShares(user1.address, user1SharesToMint); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1TokensMinted); + + await rebasableProxied.connect(owner).burnShares(user1.address, user1SharesToBurn); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1Tokens)); + + // // user2 + const user2SharesToMint = wei`64 ether`; + const user2TokensMinted = rate.mul(user2SharesToMint).div(decimals); + + const user2SharesToBurn = wei`22 ether`; + const user2TokensBurned = rate.mul(user2SharesToBurn).div(decimals); + + const user2Shares = BigNumber.from(user2SharesToMint).sub(user2SharesToBurn); + const user2Tokens = user2TokensMinted.sub(user2TokensBurned); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + + await rebasableProxied.connect(owner).mintShares(user2.address, user2SharesToMint); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); + await rebasableProxied.connect(owner).burnShares(user2.address, user2SharesToBurn); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1Tokens).add(user2Tokens)); }) .test("approve()", async (ctx) => { const { rebasableProxied } = ctx.contracts; - const { user1, user2 } = ctx.accounts; + const { holder, spender } = ctx.accounts; // validate initially allowance is zero assert.equalBN( - await rebasableProxied.allowance(user1.address, user2.address), + await rebasableProxied.allowance(holder.address, spender.address), "0" ); - const amount = 3; + const amount = wei`1 ether`; // validate return value of the method assert.isTrue( - await rebasableProxied.callStatic.approve(user2.address, amount) + await rebasableProxied.callStatic.approve(spender.address, amount) ); // approve tokens - const tx = await rebasableProxied.approve(user2.address, amount); + const tx = await rebasableProxied.approve(spender.address, amount); // validate Approval event was emitted await assert.emits(rebasableProxied, tx, "Approval", [ - user1.address, - user2.address, + holder.address, + spender.address, amount, ]); // validate allowance was set assert.equalBN( - await rebasableProxied.allowance(user1.address, user2.address), + await rebasableProxied.allowance(holder.address, spender.address), amount ); }) + .test("transfer() :: sender is zero address", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + + const { + accounts: { zero, recipient }, + } = ctx; + await assert.revertsWith( + rebasableProxied.connect(zero).transfer(recipient.address, wei`1 ether`), + "ErrorAccountIsZeroAddress()" + ); + }) + + .test("transfer() :: recipient is zero address", async (ctx) => { + const { zero, holder } = ctx.accounts; + await assert.revertsWith( + ctx.contracts.rebasableProxied.connect(holder).transfer(zero.address, wei`1 ether`), + "ErrorAccountIsZeroAddress()" + ); + }) + + .test("transfer() :: zero balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder } = ctx.accounts; + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + // transfer tokens + await rebasableProxied.connect(holder).transfer(recipient.address, "0"); + + // validate balance stays same + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + }) + + .test("transfer() :: not enough balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder } = ctx.accounts; + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = premintTokens.add(wei`1 ether`); + + // transfer tokens + await assert.revertsWith( + rebasableProxied.connect(holder).transfer(recipient.address, amount), + "ErrorNotEnoughBalance()" + ); + }) + + .test("transfer()", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder } = ctx.accounts; + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + // transfer tokens + const tx = await rebasableProxied + .connect(holder) + .transfer(recipient.address, amount); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + recipient.address, + amount, + ]); + + // validate balance was updated + assert.equalBN( + await rebasableProxied.balanceOf(holder.address), + premintTokens.sub(amount) + ); + + // validate total supply stays same + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); + }) + + .test("transferFrom()", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // holder sets allowance for spender + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance is set + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + const holderBalanceBefore = await rebasableProxied.balanceOf(holder.address); + + // transfer tokens + const tx = await rebasableProxied + .connect(spender) + .transferFrom(holder.address, recipient.address, amount); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + wei.toBigNumber(initialAllowance).sub(amount), + ]); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + recipient.address, + amount, + ]); + + // validate allowance updated + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + wei.toBigNumber(initialAllowance).sub(amount) + ); + + // validate holder balance updated + assert.equalBN( + await rebasableProxied.balanceOf(holder.address), + holderBalanceBefore.sub(amount) + ); + + const recipientBalance = await rebasableProxied.balanceOf(recipient.address); + + // validate recipient balance updated + assert.equalBN(BigNumber.from(amount).sub(recipientBalance), "1"); + }) + + .test("transferFrom() :: max allowance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder, spender } = ctx.accounts; + + const initialAllowance = hre.ethers.constants.MaxUint256; + + // set allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance is set + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + const holderBalanceBefore = await rebasableProxied.balanceOf(holder.address); + + // transfer tokens + const tx = await rebasableProxied + .connect(spender) + .transferFrom(holder.address, recipient.address, amount); + + // validate Approval event was not emitted + await assert.notEmits(rebasableProxied, tx, "Approval"); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + recipient.address, + amount, + ]); + + // validate allowance wasn't changed + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // validate holder balance updated + assert.equalBN( + await rebasableProxied.balanceOf(holder.address), + holderBalanceBefore.sub(amount) + ); + + // validate recipient balance updated + const recipientBalance = await rebasableProxied.balanceOf(recipient.address); + assert.equalBN(BigNumber.from(amount).sub(recipientBalance), "1"); + }) + + .test("transferFrom() :: not enough allowance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder, spender } = ctx.accounts; + + const initialAllowance = wei`0.9 ether`; + + // set allowance + await rebasableProxied.approve(recipient.address, initialAllowance); + + // validate allowance is set + assert.equalBN( + await rebasableProxied.allowance(holder.address, recipient.address), + initialAllowance + ); + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + // transfer tokens + await assert.revertsWith( + rebasableProxied + .connect(spender) + .transferFrom(holder.address, recipient.address, amount), + "ErrorNotEnoughAllowance()" + ); + }) + + .test("increaseAllowance() :: initial allowance is zero", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + "0" + ); + + const allowanceIncrease = wei`1 ether`; + + // increase allowance + const tx = await rebasableProxied.increaseAllowance( + spender.address, + allowanceIncrease + ); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + allowanceIncrease, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + allowanceIncrease + ); + }) + + .test("increaseAllowance() :: initial allowance is not zero", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // set initial allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + const allowanceIncrease = wei`1 ether`; + + // increase allowance + const tx = await rebasableProxied.increaseAllowance( + spender.address, + allowanceIncrease + ); + + const expectedAllowance = wei + .toBigNumber(initialAllowance) + .add(allowanceIncrease); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + expectedAllowance, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + expectedAllowance + ); + }) + + .test("increaseAllowance() :: the increase is not zero", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // set initial allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // increase allowance + const tx = await rebasableProxied.increaseAllowance(spender.address, "0"); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + initialAllowance, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + }) + + .test( + "decreaseAllowance() :: decrease is greater than current allowance", + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + "0" + ); + + const allowanceDecrease = wei`1 ether`; + + // decrease allowance + await assert.revertsWith( + rebasableProxied.decreaseAllowance(spender.address, allowanceDecrease), + "ErrorDecreasedAllowanceBelowZero()" + ); + } + ) + + .group([wei`1 ether`, "0"], (allowanceDecrease) => [ + `decreaseAllowance() :: the decrease is ${allowanceDecrease} wei`, + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // set initial allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // decrease allowance + const tx = await rebasableProxied.decreaseAllowance( + spender.address, + allowanceDecrease + ); + + const expectedAllowance = wei + .toBigNumber(initialAllowance) + .sub(allowanceDecrease); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + expectedAllowance, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + expectedAllowance + ); + }, + ]) + + .test("bridgeMint() :: not owner", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { stranger } = ctx.accounts; + + await assert.revertsWith( + rebasableProxied + .connect(stranger) + .mintShares(stranger.address, wei`1000 ether`), + "ErrorNotBridge()" + ); + }) + + .group([wei`1000 ether`, "0"], (mintAmount) => [ + `bridgeMint() :: amount is ${mintAmount} wei`, + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintShares } = ctx.constants; + const { recipient, owner } = ctx.accounts; + + // validate balance before mint + assert.equalBN(await rebasableProxied.balanceOf(recipient.address), 0); + + // validate total supply before mint + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + + // mint tokens + const tx = await rebasableProxied + .connect(owner) + .mintShares(recipient.address, mintAmount); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + hre.ethers.constants.AddressZero, + recipient.address, + mintAmount, + ]); + + // validate balance was updated + assert.equalBN( + await rebasableProxied.sharesOf(recipient.address), + mintAmount + ); + + // validate total supply was updated + assert.equalBN( + await rebasableProxied.getTotalShares(), + premintShares.add(mintAmount) + ); + }, + ]) + + .test("bridgeBurn() :: not owner", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, stranger } = ctx.accounts; + + await assert.revertsWith( + rebasableProxied.connect(stranger).burnShares(holder.address, wei`100 ether`), + "ErrorNotBridge()" + ); + }) + + .test("bridgeBurn() :: amount exceeds balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { owner, stranger } = ctx.accounts; + + // validate stranger has no tokens + assert.equalBN(await rebasableProxied.balanceOf(stranger.address), 0); + + await assert.revertsWith( + rebasableProxied.connect(owner).burnShares(stranger.address, wei`100 ether`), + "ErrorNotEnoughBalance()" + ); + }) + + .group([wei`10 ether`, "0"], (burnAmount) => [ + `bridgeBurn() :: amount is ${burnAmount} wei`, + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintShares } = ctx.constants; + const { owner, holder } = ctx.accounts; + + // validate balance before mint + assert.equalBN(await rebasableProxied.sharesOf(holder.address), premintShares); + + // validate total supply before mint + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + + // burn tokens + const tx = await rebasableProxied + .connect(owner) + .burnShares(holder.address, burnAmount); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + hre.ethers.constants.AddressZero, + burnAmount, + ]); + + const expectedBalanceAndTotalSupply = premintShares + .sub(burnAmount); + + // validate balance was updated + assert.equalBN( + await rebasableProxied.sharesOf(holder.address), + expectedBalanceAndTotalSupply + ); + + // validate total supply was updated + assert.equalBN( + await rebasableProxied.getTotalShares(), + expectedBalanceAndTotalSupply + ); + }, + ]) + .run(); async function ctxFactory() { const name = "StETH Test Token"; const symbol = "StETH"; - const decimals = 18; - const [deployer, user1, user2] = await hre.ethers.getSigners(); + const decimalsToSet = 16; + const decimals = BigNumber.from(10).pow(decimalsToSet); + const rate = BigNumber.from('12').pow(decimalsToSet - 1); + const premintShares = wei.toBigNumber(wei`100 ether`); + const premintTokens = BigNumber.from(rate).mul(premintShares).div(decimals); + + const [ + deployer, + owner, + recipient, + spender, + holder, + stranger, + user1, + user2 + ] = await hre.ethers.getSigners(); const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - - const tokensRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); - + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( - wrappedTokenStub.address, - tokensRateOracleStub.address, name, symbol, - decimals + decimalsToSet, + wrappedTokenStub.address, + tokenRateOracleStub.address, + owner.address ); - rebasableTokenImpl.wrap + await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [hre.ethers.constants.AddressZero], }); - + + const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( rebasableTokenImpl.address, deployer.address, @@ -267,12 +966,18 @@ async function ctxFactory() { const rebasableProxied = ERC20Rebasable__factory.connect( l2TokensProxy.address, - user1 + holder ); - + + await tokenRateOracleStub.setDecimals(decimalsToSet); + await tokenRateOracleStub.setLatestRoundDataAnswer(rate); + await tokenRateOracleStub.setUpdatedAt(1000); + + await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); + return { - accounts: { deployer, user1, user2 }, - constants: { name, symbol, decimals }, - contracts: { rebasableProxied, wrappedTokenStub, tokensRateOracleStub } + accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, + constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate }, + contracts: { rebasableProxied, wrappedTokenStub, tokenRateOracleStub } }; } diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index 4a3625a3..33b3fdb0 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -38,7 +38,7 @@ export default function deployment( async erc20TokenBridgeDeployScript( l1Token: string, l1TokenRebasable: string, - tokensRateOracleStub: string, + tokenRateOracleStub: string, l1Params: OptL1DeployScriptParams, l2Params: OptL2DeployScriptParams, ) { @@ -141,7 +141,7 @@ export default function deployment( factory: ERC20Rebasable__factory, args: [ expectedL2TokenProxyAddress, - tokensRateOracleStub, + tokenRateOracleStub, l2TokenRebasableName, l2TokenRebasableSymbol, decimals, @@ -174,7 +174,6 @@ export default function deployment( l1TokenRebasable, expectedL2TokenProxyAddress, expectedL2TokenRebasableProxyAddress, - tokensRateOracleStub, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 721ed54b..4042a351 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -10,7 +10,7 @@ import { ERC20Bridged__factory, ERC20BridgedStub__factory, ERC20WrapableStub__factory, - TokensRateOracleStub__factory, + TokenRateOracleStub__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, CrossDomainMessengerStub__factory, @@ -164,7 +164,7 @@ async function loadDeployedBridges( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), - tokensRateOracle: TokensRateOracleStub__factory.connect( + tokenRateOracle: TokenRateOracleStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), @@ -201,17 +201,17 @@ async function deployTestBridge( "TT" ); - const tokensRateOracleStub = await new TokensRateOracleStub__factory(optDeployer).deploy(); - await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); - await tokensRateOracleStub.setDecimals(18); - await tokensRateOracleStub.setUpdatedAt(100); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(optDeployer).deploy(); + await tokenRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); + await tokenRateOracleStub.setDecimals(18); + await tokenRateOracleStub.setUpdatedAt(100); const [ethDeployScript, optDeployScript] = await deployment( networkName ).erc20TokenBridgeDeployScript( l1Token.address, l1TokenRebasable.address, - tokensRateOracleStub.address, + tokenRateOracleStub.address, { deployer: ethDeployer, admins: { proxy: ethDeployer.address, bridge: ethDeployer.address }, @@ -252,7 +252,7 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), l1TokenRebasable: l1TokenRebasable.connect(ethProvider), - tokensRateOracle: tokensRateOracleStub, + tokenRateOracle: tokenRateOracleStub, ...connectBridgeContracts( { l2Token: optDeployScript.getContractAddress(1),