Skip to content

Commit

Permalink
Add shares math to stETH token (Aragon-way) lidofinance#28
Browse files Browse the repository at this point in the history
* Add DePool mock with initializer
* Add shares and totalShares vars to stETH
* Add shares-related getter
* Link math to totalControlledEthers of DePool
* Fix minimal tests
  • Loading branch information
ongrid committed Oct 22, 2020
1 parent 826203f commit 4a82e97
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 83 deletions.
56 changes: 54 additions & 2 deletions contracts/0.4.24/StETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@ import "./interfaces/ISTETH.sol";

import "./lib/Pausable.sol";

import "./interfaces/IDePool.sol";


/**
* @title Implementation of a liquid version of ETH 2.0 native token
*
* ERC20 token which supports stop/resume, mint/burn mechanics. The token is operated by `IDePool`.
*/
contract StETH is ISTETH, Pausable, OZERC20, AragonApp {
using SafeMath for uint256;

/// ACL
bytes32 constant public PAUSE_ROLE = keccak256("PAUSE_ROLE");
bytes32 constant public MINT_ROLE = keccak256("MINT_ROLE");
bytes32 constant public BURN_ROLE = keccak256("BURN_ROLE");

// DePool contract serves as a source of information on the amount of pooled funds
// and acts as the 'minter' of the new shares when staker submits his funds
IDePool public dePool;

// Shares are the amounts of pooled Ether normalized to the volume of ETH1.0 Ether on the moment of system start.
// Shares represent how much of initial ether are worth all-time deposits of the given user.
// In this implementation shares replace traditional balances
mapping (address => uint256) private _shares;
// ...and totalShares replace traditional totalSupply counter.
uint256 private _totalShares;

function initialize() public onlyInit {
function initialize(IDePool _dePool) public onlyInit {
dePool = _dePool;
initialized();
}

Expand Down Expand Up @@ -140,4 +152,44 @@ contract StETH is ISTETH, Pausable, OZERC20, AragonApp {
function decimals() public pure returns (uint8) {
return 18;
}

/**
* @dev Return the amount of shares that given holder has.
* @param _holder The address of the holder
*/
function getSharesByHolder(address _holder) public view returns (uint256) {
return _shares[_holder];
}

/**
* @dev Return the amount of pooled ethers for given amount of shares
* @param _sharesAmount The amount of shares
*/
function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) {
if (_totalShares == 0) {
return 0;
}
return _sharesAmount.mul(dePool.getTotalControlledEther()).div(_totalShares);
}

/**
* @dev Return the amount of pooled ethers for given holder
* @param _holder The address of the holder
*/
function getPooledEthByHolder(address _holder) public view returns (uint256) {
uint256 holderShares = getSharesByHolder(_holder);
uint256 holderPooledEth = getPooledEthByShares(holderShares);
return holderPooledEth;
}

/**
* @dev Return the amount of shares backed by given amount of pooled Eth
* @param _pooledEthAmount The amount of pooled Eth
*/
function getSharesByPooledEth(uint256 _pooledEthAmount) public view returns (uint256) {
if (dePool.getTotalControlledEther() == 0) {
return 0;
}
return _totalShares.mul(_pooledEthAmount).div(dePool.getTotalControlledEther());
}
}
17 changes: 17 additions & 0 deletions contracts/0.4.24/test_helpers/DePoolMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
pragma solidity 0.4.24;

import "../DePool.sol";
import "./VaultMock.sol";


/**
* @dev Only for testing purposes! DePool version with some functions exposed.
*/
contract DePoolMock is DePool {

function initialize(ISTETH _token) public {
_setToken(_token);
initialized();
}

}
12 changes: 6 additions & 6 deletions test/0.4.24/depool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,20 @@ contract('DePool', ([appManager, voting, user1, user2, user3, nobody]) => {
beforeEach('deploy dao and app', async () => {
const { dao, acl } = await newDao(appManager)

// token
let proxyAddress = await newApp(dao, 'steth', stEthBase.address, appManager);
token = await StETH.at(proxyAddress);
await token.initialize();

// StakingProvidersRegistry
proxyAddress = await newApp(dao, 'staking-providers-registry', stakingProvidersRegistryBase.address, appManager);
let proxyAddress = await newApp(dao, 'staking-providers-registry', stakingProvidersRegistryBase.address, appManager);
sps = await StakingProvidersRegistry.at(proxyAddress);
await sps.initialize();

// Instantiate a proxy for the app, using the base contract as its logic implementation.
proxyAddress = await newApp(dao, 'depool', appBase.address, appManager);
app = await DePool.at(proxyAddress);

// token
proxyAddress = await newApp(dao, 'steth', stEthBase.address, appManager);
token = await StETH.at(proxyAddress);
await token.initialize(app.address);

// Set up the app's permissions.
await acl.createPermission(voting, app.address, await app.PAUSE_ROLE(), appManager, {from: appManager});
await acl.createPermission(voting, app.address, await app.MANAGE_FEE(), appManager, {from: appManager});
Expand Down
152 changes: 78 additions & 74 deletions test/0.4.24/steth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,128 +4,132 @@ const { assertBn, assertRevert, assertEvent, assertAmountOfEvents } = require('@
const { ONE_DAY, ZERO_ADDRESS, MAX_UINT64, bn, getEventArgument, injectWeb3, injectArtifacts } = require('@aragon/contract-helpers-test')

const StETH = artifacts.require('StETH')
const DePoolMock = artifacts.require('DePoolMock');


const tokens = (value) => web3.utils.toWei(value + '', 'ether');


contract('StETH', ([appManager, pool, user1, user2, user3, nobody]) => {
let appBase, app, token
let dePool, dePoolBase, stEth, stEthBase

before('deploy base app', async () => {
// Deploy the app's base contract.
appBase = await StETH.new()
stEthBase = await StETH.new()
dePoolBase = await DePoolMock.new();
});

beforeEach('deploy dao and app', async () => {
const { dao, acl } = await newDao(appManager);

// Instantiate a proxy for the app, using the base contract as its logic implementation.
const proxyAddress = await newApp(dao, 'steth', appBase.address, appManager);
app = await StETH.at(proxyAddress);
const stEthProxyAddress = await newApp(dao, 'steth', stEthBase.address, appManager);
stEth = await StETH.at(stEthProxyAddress);

// Set up the app's permissions.
await acl.createPermission(pool, app.address, await app.PAUSE_ROLE(), appManager, {from: appManager});
await acl.createPermission(pool, app.address, await app.MINT_ROLE(), appManager, {from: appManager});
await acl.createPermission(pool, app.address, await app.BURN_ROLE(), appManager, {from: appManager});
// Set up the permissions for token management
await acl.createPermission(pool, stEth.address, await stEth.PAUSE_ROLE(), appManager, {from: appManager});
await acl.createPermission(pool, stEth.address, await stEth.MINT_ROLE(), appManager, {from: appManager});
await acl.createPermission(pool, stEth.address, await stEth.BURN_ROLE(), appManager, {from: appManager});

const dePoolProxyAddress = await newApp(dao, 'depool', dePoolBase.address, appManager);
dePool = await DePoolMock.at(dePoolProxyAddress);

// Initialize the app's proxy.
await app.initialize();
token = app;
await stEth.initialize(dePool.address);
await dePool.initialize(stEth.address);

// Mint some tokens
await app.mint(user1, tokens(1000), {from: pool});
await stEth.mint(user1, tokens(1000), {from: pool});
});

it('ERC20 info is accessible', async () => {
assert.equal(await app.name(), "Liquid staked Ether 2.0");
assert.equal(await app.symbol(), "StETH");
assert.equal(await app.decimals(), 18);
assert.equal(await stEth.name(), "Liquid staked Ether 2.0");
assert.equal(await stEth.symbol(), "StETH");
assert.equal(await stEth.decimals(), 18);
});

it('ERC20 methods are supported', async () => {
assertBn(await token.totalSupply({from: nobody}), tokens(1000));
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(1000));
assertBn(await stEth.totalSupply({from: nobody}), tokens(1000));
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(1000));

// transfer
await token.transfer(user2, tokens(2), {from: user1});
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(998));
assertBn(await token.balanceOf(user2, {from: nobody}), tokens(2));
await stEth.transfer(user2, tokens(2), {from: user1});
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(998));
assertBn(await stEth.balanceOf(user2, {from: nobody}), tokens(2));

await assertRevert(token.transfer(user2, tokens(2), {from: user3}));
await assertRevert(token.transfer(user3, tokens(2000), {from: user1}));
await assertRevert(stEth.transfer(user2, tokens(2), {from: user3}));
await assertRevert(stEth.transfer(user3, tokens(2000), {from: user1}));

// approve
await token.approve(user2, tokens(3), {from: user1});
assertBn(await token.allowance(user1, user2, {from: nobody}), tokens(3));
await token.transferFrom(user1, user3, tokens(2), {from: user2});
assertBn(await token.allowance(user1, user2, {from: nobody}), tokens(1));
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(996));
assertBn(await token.balanceOf(user2, {from: nobody}), tokens(2));
assertBn(await token.balanceOf(user3, {from: nobody}), tokens(2));

await assertRevert(token.transferFrom(user1, user3, tokens(2), {from: user2}));
await assertRevert(token.transferFrom(user2, user3, tokens(2), {from: user2}));
await assertRevert(token.transferFrom(user1, user3, tokens(2), {from: user3}));
await assertRevert(token.transferFrom(user2, user3, tokens(2), {from: user3}));
await stEth.approve(user2, tokens(3), {from: user1});
assertBn(await stEth.allowance(user1, user2, {from: nobody}), tokens(3));
await stEth.transferFrom(user1, user3, tokens(2), {from: user2});
assertBn(await stEth.allowance(user1, user2, {from: nobody}), tokens(1));
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(996));
assertBn(await stEth.balanceOf(user2, {from: nobody}), tokens(2));
assertBn(await stEth.balanceOf(user3, {from: nobody}), tokens(2));

await assertRevert(stEth.transferFrom(user1, user3, tokens(2), {from: user2}));
await assertRevert(stEth.transferFrom(user2, user3, tokens(2), {from: user2}));
await assertRevert(stEth.transferFrom(user1, user3, tokens(2), {from: user3}));
await assertRevert(stEth.transferFrom(user2, user3, tokens(2), {from: user3}));
});

it('minting works', async () => {
await token.mint(user1, tokens(12), {from: pool});
await token.mint(user2, tokens(4), {from: pool});
assertBn(await token.totalSupply(), tokens(1016));
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(1012));
assertBn(await token.balanceOf(user2, {from: nobody}), tokens(4));
await stEth.mint(user1, tokens(12), {from: pool});
await stEth.mint(user2, tokens(4), {from: pool});
assertBn(await stEth.totalSupply(), tokens(1016));
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(1012));
assertBn(await stEth.balanceOf(user2, {from: nobody}), tokens(4));

for (const acc of [user1, user2, user3, nobody])
await assertRevert(token.mint(user2, tokens(4), {from: acc}), 'APP_AUTH_FAILED');
await assertRevert(stEth.mint(user2, tokens(4), {from: acc}), 'APP_AUTH_FAILED');
});

it('stop/resume works', async () => {
await token.transfer(user2, tokens(2), {from: user1});
assert.equal(await token.isStopped(), false);

await assertRevert(token.stop({from: user1}));
await token.stop({from: pool});
await assertRevert(token.stop({from: pool}));
assert(await token.isStopped());

await assertRevert(token.transfer(user2, tokens(2), {from: user1}), 'CONTRACT_IS_STOPPED');
await assertRevert(token.transfer(user2, tokens(2), {from: user3}));
await assertRevert(token.transferFrom(user1, user3, tokens(2), {from: user2}));

await assertRevert(token.resume({from: user1}));
await token.resume({from: pool});
await assertRevert(token.resume({from: pool}));
assert.equal(await token.isStopped(), false);

await token.transfer(user2, tokens(2), {from: user1});
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(996));
assertBn(await token.balanceOf(user2, {from: nobody}), tokens(4));
await stEth.transfer(user2, tokens(2), {from: user1});
assert.equal(await stEth.isStopped(), false);

await assertRevert(stEth.stop({from: user1}));
await stEth.stop({from: pool});
await assertRevert(stEth.stop({from: pool}));
assert(await stEth.isStopped());

await assertRevert(stEth.transfer(user2, tokens(2), {from: user1}), 'CONTRACT_IS_STOPPED');
await assertRevert(stEth.transfer(user2, tokens(2), {from: user3}));
await assertRevert(stEth.transferFrom(user1, user3, tokens(2), {from: user2}));

await assertRevert(stEth.resume({from: user1}));
await stEth.resume({from: pool});
await assertRevert(stEth.resume({from: pool}));
assert.equal(await stEth.isStopped(), false);

await stEth.transfer(user2, tokens(2), {from: user1});
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(996));
assertBn(await stEth.balanceOf(user2, {from: nobody}), tokens(4));
});

it('burning works', async () => {
await token.transfer(user2, tokens(2), {from: user1});
await stEth.transfer(user2, tokens(2), {from: user1});

await token.burn(user1, tokens(2), {from: pool});
await token.burn(user2, tokens(1), {from: pool});
await stEth.burn(user1, tokens(2), {from: pool});
await stEth.burn(user2, tokens(1), {from: pool});

assertBn(await token.totalSupply(), tokens(997));
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(996));
assertBn(await token.balanceOf(user2, {from: nobody}), tokens(1));
assertBn(await stEth.totalSupply(), tokens(997));
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(996));
assertBn(await stEth.balanceOf(user2, {from: nobody}), tokens(1));

for (const acc of [user1, user2, user3, nobody]) {
await assertRevert(token.burn(user1, tokens(4), {from: acc}), 'APP_AUTH_FAILED');
await assertRevert(token.burn(user3, tokens(4), {from: acc}), 'APP_AUTH_FAILED');
await assertRevert(stEth.burn(user1, tokens(4), {from: acc}), 'APP_AUTH_FAILED');
await assertRevert(stEth.burn(user3, tokens(4), {from: acc}), 'APP_AUTH_FAILED');
}

await assertRevert(token.burn(user2, tokens(4), {from: pool}));
await assertRevert(stEth.burn(user2, tokens(4), {from: pool}));

await token.burn(user1, tokens(96), {from: pool});
await token.burn(user2, tokens(1), {from: pool});
await stEth.burn(user1, tokens(96), {from: pool});
await stEth.burn(user2, tokens(1), {from: pool});

assertBn(await token.totalSupply(), tokens(900));
assertBn(await token.balanceOf(user1, {from: nobody}), tokens(900));
assertBn(await token.balanceOf(user2, {from: nobody}), 0);
assertBn(await stEth.totalSupply(), tokens(900));
assertBn(await stEth.balanceOf(user1, {from: nobody}), tokens(900));
assertBn(await stEth.balanceOf(user2, {from: nobody}), 0);
});
});
2 changes: 1 addition & 1 deletion test/scenario/helpers/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function deployDaoAndPool(appManager, voting, depositIterationLimit = 10)

// Initialize the token, the SP registry and the pool

await token.initialize()
await token.initialize(pool.address)
await spRegistry.initialize()

const [
Expand Down

0 comments on commit 4a82e97

Please sign in to comment.