Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit 2a42cb5

Browse files
authored
Merge pull request #16 from lidofinance/fix/rounding
Fix AStETH rounding issue
2 parents 7a66ffd + eeecf29 commit 2a42cb5

22 files changed

+1397
-700
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ To run tests for StETH integration use the following commands:
125125

126126
```
127127
npm install
128+
npm run compile
128129
npm run test:steth
129130
npm run test:steth-coverage # to run tests with coverage report
130131
```

contracts/mocks/tokens/StETHMocked.sol

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ pragma solidity 0.6.12;
99
contract StETHMocked {
1010
using LidoSafeMath for uint256;
1111

12-
uint256 internal _totalShares;
13-
uint256 internal _pooledEther;
12+
// use there values like real stETH has
13+
uint256 internal _totalShares = 1608965089698263670456320;
14+
uint256 internal _pooledEther = 1701398689820002221426255;
1415
mapping(address => uint256) private shares;
1516
mapping(address => mapping(address => uint256)) private allowances;
1617

@@ -200,7 +201,7 @@ contract StETHMocked {
200201
}
201202

202203
_mintShares(sender, sharesAmount);
203-
_pooledEther = _pooledEther.add(sharesAmount);
204+
_pooledEther = _pooledEther.add(deposit);
204205
return sharesAmount;
205206
}
206207

contracts/protocol/tokenization/lido/AStETH.sol

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
105105
uint256 amount,
106106
uint256 index
107107
) external override onlyLendingPool {
108-
uint256 amountScaled = amount.rayDiv(_stEthRebasingIndex()).rayDiv(index);
108+
uint256 amountScaled = _toInternalAmount(amount, _stEthRebasingIndex(), index);
109109
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
110110
_burn(user, amountScaled);
111111

@@ -130,7 +130,7 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
130130
) external override onlyLendingPool returns (bool) {
131131
uint256 previousBalance = super.balanceOf(user);
132132

133-
uint256 amountScaled = amount.rayDiv(_stEthRebasingIndex()).rayDiv(index);
133+
uint256 amountScaled = _toInternalAmount(amount, _stEthRebasingIndex(), index);
134134
require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);
135135
_mint(user, amountScaled);
136136

@@ -152,10 +152,10 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
152152
}
153153

154154
// Compared to the normal mint, we don't check for rounding errors.
155-
// The amount to mint can easily be very small since it is a fraction of the interest ccrued.
155+
// The amount to mint can easily be very small since it is a fraction of the interest accrued.
156156
// In that case, the treasury will experience a (very small) loss, but it
157-
// wont cause potentially valid transactions to fail.
158-
_mint(RESERVE_TREASURY_ADDRESS, amount.rayDiv(_stEthRebasingIndex()).rayDiv(index));
157+
// won't cause potentially valid transactions to fail.
158+
_mint(RESERVE_TREASURY_ADDRESS, _toInternalAmount(amount, _stEthRebasingIndex(), index));
159159

160160
emit Transfer(address(0), RESERVE_TREASURY_ADDRESS, amount);
161161
emit Mint(RESERVE_TREASURY_ADDRESS, amount, index);
@@ -207,6 +207,17 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
207207
return _scaledBalanceOf(user, _stEthRebasingIndex());
208208
}
209209

210+
/**
211+
* @dev Returns the internal balance of the user. The internal balance is the balance of
212+
* the underlying asset of the user (sum of deposits of the user), divided by the current
213+
* liquidity index at the moment of the update and by the current stETH rebasing index.
214+
* @param user The user whose balance is calculated
215+
* @return The internal balance of the user
216+
**/
217+
function internalBalanceOf(address user) external view returns (uint256) {
218+
return super.balanceOf(user);
219+
}
220+
210221
/**
211222
* @dev Returns the scaled balance of the user and the scaled total supply.
212223
* @param user The address of the user
@@ -247,6 +258,15 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
247258
return _scaledTotalSupply(_stEthRebasingIndex());
248259
}
249260

261+
/**
262+
* @dev Returns the internal total supply of the token. Represents
263+
* sum(debt/_stEthRebasingIndex/liquidityIndex).
264+
* @return the internal total supply
265+
*/
266+
function internalTotalSupply() external view returns (uint256) {
267+
return super.totalSupply();
268+
}
269+
250270
/**
251271
* @dev Transfers the underlying asset to `target`. Used by the LendingPool to transfer
252272
* assets in borrow(), withdraw() and flashLoan()
@@ -315,15 +335,17 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
315335
uint256 amount,
316336
bool validate
317337
) internal {
318-
uint256 index = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS);
338+
uint256 aaveLiquidityIndex = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS);
339+
uint256 stEthRebasingIndex = _stEthRebasingIndex();
319340

320-
uint256 rebasingIndex = _stEthRebasingIndex();
321-
uint256 fromBalanceBefore = _scaledBalanceOf(from, rebasingIndex).rayMul(index);
322-
uint256 toBalanceBefore = _scaledBalanceOf(to, rebasingIndex).rayMul(index);
341+
uint256 fromBalanceBefore =
342+
_scaledBalanceOf(from, stEthRebasingIndex).rayMul(aaveLiquidityIndex);
343+
uint256 toBalanceBefore = _scaledBalanceOf(to, stEthRebasingIndex).rayMul(aaveLiquidityIndex);
323344

324-
super._transfer(from, to, amount.rayDiv(rebasingIndex).rayDiv(index));
345+
super._transfer(from, to, _toInternalAmount(amount, stEthRebasingIndex, aaveLiquidityIndex));
325346

326347
if (validate) {
348+
require(fromBalanceBefore >= amount, 'ERC20: transfer amount exceeds balance');
327349
POOL.finalizeTransfer(
328350
UNDERLYING_ASSET_ADDRESS,
329351
from,
@@ -334,7 +356,7 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
334356
);
335357
}
336358

337-
emit BalanceTransfer(from, to, amount, index);
359+
emit BalanceTransfer(from, to, amount, aaveLiquidityIndex);
338360
}
339361

340362
/**
@@ -351,42 +373,33 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken {
351373
_transfer(from, to, amount, true);
352374
}
353375

354-
/**
355-
* @return Current rebasin index of stETH in RAY
356-
**/
357-
function _stEthRebasingIndex() internal view returns (uint256) {
358-
// Below expression returns how much Ether corresponds
359-
// to 10 ** 27 shares. 10 ** 27 was taken to provide
360-
// same precision as AAVE's liquidity index, which
361-
// counted in RAY's (decimals with 27 digits).
362-
return ILido(UNDERLYING_ASSET_ADDRESS).getPooledEthByShares(1e27);
363-
}
364-
365376
function _scaledBalanceOf(address user, uint256 rebasingIndex) internal view returns (uint256) {
366-
return super.balanceOf(user).rayMul(rebasingIndex);
377+
return super.balanceOf(user).mul(rebasingIndex).div(WadRayMath.RAY);
367378
}
368379

369380
function _scaledTotalSupply(uint256 rebasingIndex) internal view returns (uint256) {
370-
return super.totalSupply().rayMul(rebasingIndex);
381+
return super.totalSupply().mul(rebasingIndex).div(WadRayMath.RAY);
371382
}
372383

373384
/**
374-
* @dev Returns the internal balance of the user. The internal balance is the balance of
375-
* the underlying asset of the user (sum of deposits of the user), divided by the current
376-
* liquidity index at the moment of the update and by the current stETH rebasing index.
377-
* @param user The user whose balance is calculated
378-
* @return The internal balance of the user
385+
* @return Current rebasing index of stETH in RAY
379386
**/
380-
function internalBalanceOf(address user) external view returns (uint256) {
381-
return super.balanceOf(user);
387+
function _stEthRebasingIndex() internal view returns (uint256) {
388+
// Returns amount of stETH corresponding to 10**27 stETH shares.
389+
// The 10**27 is picked to provide the same precision as the AAVE
390+
// liquidity index, which is in RAY (10**27).
391+
return ILido(UNDERLYING_ASSET_ADDRESS).getPooledEthByShares(WadRayMath.RAY);
382392
}
383393

384394
/**
385-
* @dev Returns the internal total supply of the token. Represents
386-
* sum(debt/_stEthRebasingIndex/liquidityIndex).
387-
* @return the internal total supply
395+
* @dev Converts amount of astETH to internal shares, based
396+
* on stEthRebasingIndex and aaveLiquidityIndex.
388397
*/
389-
function internalTotalSupply() external view returns (uint256) {
390-
return super.totalSupply();
398+
function _toInternalAmount(
399+
uint256 amount,
400+
uint256 stEthRebasingIndex,
401+
uint256 aaveLiquidityIndex
402+
) internal view returns (uint256) {
403+
return amount.mul(WadRayMath.RAY).div(stEthRebasingIndex).rayDiv(aaveLiquidityIndex);
391404
}
392405
}

contracts/protocol/tokenization/lido/README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function _stEthRebasingIndex() returns (uint256) {
5757
// to 10 ** 27 shares. 10 ** 27 was taken to provide
5858
// same precision as AAVE's liquidity index, which
5959
// counted in RAY's (decimals with 27 digits).
60-
return stETH.getPooledEthByShares(10**27);
60+
return stETH.getPooledEthByShares(WadRayMath.RAY);
6161
}
6262
6363
```
@@ -68,27 +68,35 @@ With stETH rebasing index, `AStETH` allows to make rebases profit accountable, a
6868
function mint(address user, uint256 amount, uint256 liquidityIndex) {
6969
...
7070
uint256 stEthRebasingIndex = _stEthRebasingIndex();
71-
_mint(user, amount.rayDiv(stEthRebasingIndex).rayDiv(liquidityIndex));
71+
_mint(user, _toInternalAmount(amount, stEthRebasingIndex, liquidityIndex));
7272
...
7373
}
7474
7575
function burn(address user, uint256 amount, uint256 liquidityIndex) {
7676
...
7777
uint256 stEthRebasingIndex = _stEthRebasingIndex();
78-
_burn(user, amount.rayDiv(stEthRebasingIndex)).rayDiv(liquidityIndex);
78+
_burn(user, _toInternalAmount(amount, stEthRebasingIndex, liquidityIndex));
7979
...
8080
}
81+
82+
function _toInternalAmount(
83+
uint256 amount,
84+
uint256 stEthRebasingIndex,
85+
uint256 aaveLiquidityIndex
86+
) internal view returns (uint256) {
87+
return amount.mul(WadRayMath.RAY).div(stEthRebasingIndex).rayDiv(aaveLiquidityIndex);
88+
}
8189
```
8290

8391
Then, according to AAVE's definitions, `scaledTotalSupply()` and `scaledBalanceOf()` might be calculated as:
8492

8593
```solidity=
8694
function scaledTotalSupply() returns (uint256) {
87-
return _totalSupply.rayMul(_stEthRebasingIndex());
95+
return _totalSupply.mul(_stEthRebasingIndex()).div(WadRayMath.RAY);
8896
}
8997
9098
function scaledBalanceOf(address user) returns (uint256) {
91-
return _balances[user].rayMul(_stEthRebasingIndex());
99+
return _balances[user].mul(_stEthRebasingIndex()).div(WadRayMath.RAY);
92100
}
93101
94102
```

test/astETH/__setup.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import bignumberChai from 'chai-bignumber';
66
import { solidity } from 'ethereum-waffle';
77
import { AstEthSetup } from './init';
88
import '../helpers/utils/math';
9+
import { wei } from './helpers';
910

1011
chai.use(bignumberChai());
1112
chai.use(almostEqual());
@@ -15,6 +16,7 @@ let setup: AstEthSetup, evmSnapshotId;
1516

1617
before(async () => {
1718
setup = await AstEthSetup.deploy();
19+
await setup.priceFeed.setPrice(wei`0.99 ether`);
1820
evmSnapshotId = await hre.ethers.provider.send('evm_snapshot', []);
1921
});
2022

test/astETH/asserts.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import BigNumber from 'bignumber.js';
2+
import { expect } from 'chai';
3+
import { toWei } from './helpers';
4+
import { AstEthSetup, Lender } from './init';
5+
6+
export function lt(actual: string, expected: string, message?: string) {
7+
expect(actual).to.be.bignumber.lt(expected, message);
8+
}
9+
10+
export function gt(actual: string, expected: string, message?: string) {
11+
expect(actual).to.be.bignumber.gt(expected, message);
12+
}
13+
14+
export function eq(actual: string, expected: string, message?: string) {
15+
expect(actual).is.equal(expected, message);
16+
}
17+
18+
export function almostEq(actual: string, expected: string, epsilon: string = '1') {
19+
const lowerBound = new BigNumber(expected).minus(epsilon).toString();
20+
const upperBound = new BigNumber(expected).plus(epsilon).toString();
21+
expect(actual).to.be.bignumber.lte(upperBound);
22+
expect(actual).to.be.bignumber.gte(lowerBound);
23+
}
24+
25+
export function lte(actual: string, expected: string, epsilon: string = '1') {
26+
const lowerBound = new BigNumber(expected).minus(epsilon).toString();
27+
expect(actual).to.be.bignumber.lte(expected);
28+
expect(actual).to.be.bignumber.gte(lowerBound);
29+
}
30+
31+
export function gte(actual: string, expected: string, epsilon: string = '1') {
32+
const upperBound = new BigNumber(expected).plus(epsilon).toString();
33+
expect(actual).to.be.bignumber.gte(expected);
34+
expect(actual).to.be.bignumber.lte(upperBound);
35+
}
36+
37+
export async function astEthBalance(
38+
lender: Lender,
39+
expectedBalance: string,
40+
epsilon: string = '1'
41+
) {
42+
const [balance, internalBalance, liquidityIndex] = await Promise.all([
43+
lender.astEthBalance(),
44+
lender.astEthInternalBalance(),
45+
lender.lendingPool.getReserveNormalizedIncome(lender.stETH.address).then(toWei),
46+
]);
47+
lte(balance, expectedBalance, epsilon);
48+
// to validate that amount of shares is correct
49+
// we convert internal balance to stETH shares and assert with astETH balance
50+
const fromInternalBalance = await lender.stETH.getPooledEthByShares(internalBalance).then(toWei);
51+
eq(
52+
new BigNumber(fromInternalBalance).rayMul(new BigNumber(liquidityIndex)).toFixed(0),
53+
balance,
54+
`Unexpected astETH.internalBalanceOf() value`
55+
);
56+
}
57+
58+
export async function astEthTotalSupply(
59+
setup: AstEthSetup,
60+
expectedValue: string,
61+
epsilon: string = '1'
62+
) {
63+
const [totalSupply, internalTotalSupply, stEthBalance, liquidityIndex] = await Promise.all([
64+
setup.astEthTotalSupply(),
65+
setup.astETH.internalTotalSupply().then(toWei),
66+
setup.stETH.balanceOf(setup.astETH.address).then(toWei),
67+
setup.aave.lendingPool.getReserveNormalizedIncome(setup.stETH.address).then(toWei),
68+
]);
69+
70+
lte(totalSupply, expectedValue, epsilon);
71+
// to validate that internal number of shares is correct
72+
// internal total supply converts to stETH and assert it with astETH total supply
73+
const fromInternalTotalSupply = await setup.stETH
74+
.getPooledEthByShares(internalTotalSupply)
75+
.then(toWei);
76+
eq(
77+
new BigNumber(fromInternalTotalSupply).rayMul(new BigNumber(liquidityIndex)).toFixed(0),
78+
totalSupply,
79+
`Unexpected astETH.internalTotalSupply()`
80+
);
81+
eq(
82+
totalSupply,
83+
stEthBalance,
84+
`astETH.totalSupply() is ${totalSupply}, but stETH.balanceOf(astETH) is ${stEthBalance}`
85+
);
86+
}
87+
88+
export default { lt, lte, eq, almostEq, gt, gte, astEthBalance, astEthTotalSupply };
Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,45 @@
1-
import { assertBalance, wei } from './helpers';
1+
import asserts from './asserts';
2+
import { wei } from './helpers';
23
import { setup } from './__setup.spec';
34

45
describe('AStETH Allowance:', function () {
56
it('allowance', async () => {
67
const { lenderA, lenderB } = setup.lenders;
8+
79
const allowanceBefore = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
8-
assertBalance(allowanceBefore.toString(), wei(0));
9-
await lenderA.astETH.approve(lenderB.address, wei(10));
10+
asserts.eq(allowanceBefore.toString(), wei`0`);
11+
12+
await lenderA.astETH.approve(lenderB.address, wei`10 ether`);
1013
const allowanceAfter = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
11-
assertBalance(allowanceAfter.toString(), wei(10));
14+
asserts.eq(allowanceAfter.toString(), wei`10 ether`);
1215
});
1316
it('decreaseAllowance', async () => {
1417
const { lenderA, lenderB } = setup.lenders;
18+
1519
const allowanceBefore = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
16-
assertBalance(allowanceBefore.toString(), wei(0));
17-
await lenderA.astETH.approve(lenderB.address, wei(10));
20+
asserts.eq(allowanceBefore.toString(), wei`0`);
21+
22+
await lenderA.astETH.approve(lenderB.address, wei`10 ether`);
1823
const allowanceAfter = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
19-
assertBalance(allowanceAfter.toString(), wei(10));
24+
asserts.eq(allowanceAfter.toString(), wei`10 ether`);
2025

21-
await lenderA.astETH.decreaseAllowance(lenderB.address, wei(5));
26+
await lenderA.astETH.decreaseAllowance(lenderB.address, wei`5 ether`);
2227
const allowanceAfterDecrease = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
23-
assertBalance(allowanceAfterDecrease.toString(), wei(5));
28+
asserts.eq(allowanceAfterDecrease.toString(), wei`5 ether`);
2429
});
2530

2631
it('increaseAllowance', async () => {
2732
const { lenderA, lenderB } = setup.lenders;
33+
2834
const allowanceBefore = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
29-
assertBalance(allowanceBefore.toString(), wei(0));
30-
await lenderA.astETH.approve(lenderB.address, wei(10));
35+
asserts.eq(allowanceBefore.toString(), wei`0`);
36+
37+
await lenderA.astETH.approve(lenderB.address, wei`10 ether`);
3138
const allowanceAfter = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
32-
assertBalance(allowanceAfter.toString(), wei(10));
39+
asserts.eq(allowanceAfter.toString(), wei`10 ether`);
3340

34-
await lenderA.astETH.increaseAllowance(lenderB.address, wei(5));
41+
await lenderA.astETH.increaseAllowance(lenderB.address, wei`5 ether`);
3542
const allowanceAfterDecrease = await lenderA.astETH.allowance(lenderA.address, lenderB.address);
36-
assertBalance(allowanceAfterDecrease.toString(), wei(15));
43+
asserts.eq(allowanceAfterDecrease.toString(), wei`15 ether`);
3744
});
3845
});

0 commit comments

Comments
 (0)