Skip to content

Commit

Permalink
Merge pull request #18 from pooltogether/fixes/c4-audit
Browse files Browse the repository at this point in the history
C4 audit mitigations
  • Loading branch information
PierrickGT committed Jul 12, 2021
2 parents 574e36e + a79c7f2 commit efce150
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .solhint.json
Expand Up @@ -16,4 +16,4 @@
"not-rely-on-time": "off",
"reason-string": ["warn", {"maxLength": 64}]
}
}
}
2 changes: 1 addition & 1 deletion README.md
@@ -1,4 +1,4 @@
[![Coverage Status](https://coveralls.io/repos/github/steffenix/sushi-pooltogether/badge.svg?branch=master)](https://coveralls.io/github/steffenix/sushi-pooltogether?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/pooltogether/sushi-pooltogether/badge.svg)](https://coveralls.io/github/pooltogether/sushi-pooltogether)
![Tests](https://github.com/pooltogether/sushi-pooltogether/actions/workflows/test.yml/badge.svg)
![Linting](https://github.com/pooltogether/sushi-pooltogether/actions/workflows/lint.yml/badge.svg)

Expand Down
17 changes: 0 additions & 17 deletions contracts/ISushi.sol

This file was deleted.

1 change: 0 additions & 1 deletion contracts/ISushiBar.sol
Expand Up @@ -10,5 +10,4 @@ interface ISushiBar {
function totalSupply() external view returns (uint256);

function balanceOf(address account) external view returns (uint256);

}
107 changes: 77 additions & 30 deletions contracts/SushiYieldSource.sol
Expand Up @@ -2,93 +2,140 @@

pragma solidity 0.6.12;

import { IYieldSource } from "@pooltogether/yield-source-interface/contracts/IYieldSource.sol";
import "@pooltogether/yield-source-interface/contracts/IYieldSource.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import "./ISushiBar.sol";
import "./ISushi.sol";

/// @title A pooltogether yield source for sushi token
/// @author Steffel Fenix
contract SushiYieldSource is IYieldSource {

contract SushiYieldSource is IYieldSource, ReentrancyGuard {
using SafeERC20 for IERC20;
using SafeMath for uint256;


/// @notice Interface of the SushiBar contract
ISushiBar public immutable sushiBar;
ISushi public immutable sushiAddr;


/// @notice Interface for the Sushi token
IERC20 public immutable sushiAddr;

mapping(address => uint256) public balances;

constructor(ISushiBar _sushiBar, ISushi _sushiAddr) public {
/// @notice Emitted when asset tokens are redeemed from the yield source
event RedeemedToken(
address indexed from,
uint256 shares,
uint256 amount
);

/// @notice Emitted when asset tokens are supplied to the yield source
event SuppliedTokenTo(
address indexed from,
uint256 shares,
uint256 amount,
address indexed to
);

constructor(ISushiBar _sushiBar, IERC20 _sushiAddr) public ReentrancyGuard() {
require(
address(_sushiBar) != address(0),
"SushiYieldSource/sushiBar-not-zero-address"
);
require(
address(_sushiAddr) != address(0),
"SushiYieldSource/sushiAddr-not-zero-address"
);

sushiBar = _sushiBar;
sushiAddr = _sushiAddr;

_sushiAddr.safeApprove(address(_sushiBar), type(uint256).max);
}

/// @notice Approve SUSHI to spend infinite sushiBar (xSUSHI)
/// @dev Emergency function to re-approve max amount if approval amount dropped too low
/// @return true if operation is successful
function approveMaxAmount() external returns (bool) {
address _sushiBarAddress = address(sushiBar);
IERC20 sushi = sushiAddr;

uint256 allowance = sushi.allowance(address(this), _sushiBarAddress);

sushi.safeIncreaseAllowance(_sushiBarAddress, type(uint256).max.sub(allowance));
return true;
}

/// @notice Returns the ERC20 asset token used for deposits.
/// @return The ERC20 asset token
function depositToken() public view override returns (address) {
function depositToken() external view override returns (address) {
return address(sushiAddr);
}

/// @notice Returns the total balance (in asset tokens). This includes the deposits and interest.
/// @return The underlying balance of asset tokens
function balanceOfToken(address addr) public override returns (uint256) {
function balanceOfToken(address addr) external override returns (uint256) {
if (balances[addr] == 0) return 0;

uint256 totalShares = sushiBar.totalSupply();
uint256 barSushiBalance = sushiAddr.balanceOf(address(sushiBar));

return balances[addr].mul(barSushiBalance).div(totalShares);
return balances[addr].mul(barSushiBalance).div(totalShares);
}

/// @notice Allows assets to be supplied on other user's behalf using the `to` param.
/// @param amount The amount of `token()` to be supplied
/// @param to The user whose balance will receive the tokens
function supplyTokenTo(uint256 amount, address to) public override {
sushiAddr.transferFrom(msg.sender, address(this), amount);
sushiAddr.approve(address(sushiBar), amount);

function supplyTokenTo(uint256 amount, address to) external override nonReentrant {
ISushiBar bar = sushiBar;
IERC20 sushi = sushiAddr;

sushi.safeTransferFrom(msg.sender, address(this), amount);

uint256 beforeBalance = bar.balanceOf(address(this));

bar.enter(amount);

uint256 afterBalance = bar.balanceOf(address(this));
uint256 balanceDiff = afterBalance.sub(beforeBalance);

balances[to] = balances[to].add(balanceDiff);
emit SuppliedTokenTo(msg.sender, balanceDiff, amount, to);
}

/// @notice Redeems tokens from the yield source to the msg.sender, it burns yield bearing tokens and returns token to the sender.
/// @param amount The amount of `token()` to withdraw. Denominated in `token()` as above.
/// @dev The maxiumum that can be called for token() is calculated by balanceOfToken() above.
/// @return The actual amount of tokens that were redeemed. This may be different from the amount passed due to the fractional math involved.
function redeemToken(uint256 amount) public override returns (uint256) {
/// @return The actual amount of tokens that were redeemed. This may be different from the amount passed due to the fractional math involved.
function redeemToken(uint256 amount) external override nonReentrant returns (uint256) {
ISushiBar bar = sushiBar;
ISushi sushi = sushiAddr;
IERC20 sushi = sushiAddr;

uint256 totalShares = bar.totalSupply();
if(totalShares == 0) return 0;
if (totalShares == 0) return 0;

uint256 barSushiBalance = sushi.balanceOf(address(bar));
if(barSushiBalance == 0) return 0;
if (barSushiBalance == 0) return 0;

uint256 sushiBeforeBalance = sushi.balanceOf(address(this));

uint256 requiredShares = ((amount.mul(totalShares) + totalShares)).div(barSushiBalance);
if(requiredShares == 0) return 0;
uint256 requiredShares = ((amount.mul(totalShares).add(totalShares))).div(barSushiBalance);
if (requiredShares == 0) return 0;

uint256 requiredSharesBalance = requiredShares.sub(1);
bar.leave(requiredSharesBalance);

uint256 sushiAfterBalance = sushi.balanceOf(address(this));

uint256 sushiBalanceDiff = sushiAfterBalance.sub(sushiBeforeBalance);

balances[msg.sender] = balances[msg.sender].sub(requiredSharesBalance);
sushi.transfer(msg.sender, sushiBalanceDiff);


sushi.safeTransfer(msg.sender, sushiBalanceDiff);
emit RedeemedToken(msg.sender, requiredSharesBalance, amount);

return (sushiBalanceDiff);
}

}
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -21,7 +21,8 @@
"hardhat-typechain": "^0.3.5",
"prettier": "^2.2.1",
"prettier-plugin-solidity": "^1.0.0-beta.6",
"solidity-coverage": "^0.7.16",
"solhint": "3.3.6",
"solidity-coverage": "0.7.16",
"ts-generator": "^0.1.1",
"ts-node": "^9.1.1",
"typechain": "^4.0.3",
Expand Down
14 changes: 7 additions & 7 deletions scripts/test.js
Expand Up @@ -34,35 +34,35 @@ async function getYieldSourcePrizePoolProxy(tx) {
}

async function run() {
console.log("running fork script")
console.log("running fork script");

await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [SUSHI_HOLDER],
});

const SUSHI_TOKEN_ADDRESS = "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2"
const XSUSHI_ADDRESS = "0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272"
const SUSHI_TOKEN_ADDRESS = "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2";
const XSUSHI_ADDRESS = "0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272";

const sushiHolder = await ethers.provider.getUncheckedSigner(SUSHI_HOLDER);
const sushi = await ethers.getContractAt(
"IERC20Upgradeable",
SUSHI_TOKEN_ADDRESS,
sushiHolder
);
console.log("getting builder")
console.log("getting builder");
const builder = await ethers.getContractAt(
"PoolWithMultipleWinnersBuilder",
"0x39E2F33ff4Ad3491106B3BB15dc66EbE24e4E9C7"
);
console.log("deploying")
console.log("deploying");
SushiYieldSourceFactory = await ethers.getContractFactory("SushiYieldSource");
sushiYieldSource = await SushiYieldSourceFactory.deploy(
XSUSHI_ADDRESS,
SUSHI_TOKEN_ADDRESS
SUSHI_TOKEN_ADDRESS
);

console.log("deployed SushiYieldSource at ", sushiYieldSource.address)
console.log("deployed SushiYieldSource at ", sushiYieldSource.address);

const block = await ethers.provider.getBlock();

Expand Down
3 changes: 2 additions & 1 deletion test/integration_test.js
Expand Up @@ -110,7 +110,7 @@ describe("SushiYieldSource integration", function () {
{ gasLimit: 9500000 }
);

const exchangeWalletAddress = "0xD551234Ae421e3BCBA99A0Da6d736074f22192FF";
const exchangeWalletAddress = "0xF977814e90dA44bFA03b6295A0616a897441aceC";
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [exchangeWalletAddress],
Expand Down Expand Up @@ -144,6 +144,7 @@ describe("SushiYieldSource integration", function () {
"0x6B3595068778DD592e39A122f4f5a5cF09C90fE2",
{ gasLimit: 9500000 }
);

const yieldSourcePrizePoolConfig = {
yieldSource: yieldSource.address,
maxExitFeeMantissa: toWei("0.5"),
Expand Down
69 changes: 61 additions & 8 deletions test/unit_test.js
Expand Up @@ -17,13 +17,26 @@ describe("SushiYieldSource", function () {
let yieldSource;
let amount;

let SushiYieldSourceContract;

let isDeployTest = false;

const deploySushiYieldSource = async (sushiBarAddress, sushiAddress) => {
yieldSource = await SushiYieldSourceContract.deploy(
sushiBarAddress,
sushiAddress,
overrides
);
};

beforeEach(async function () {
[wallet, wallet2] = await ethers.getSigners();
const ERC20MintableContract = await hre.ethers.getContractFactory(
"ERC20Mintable",
wallet,
overrides
);

sushi = await ERC20MintableContract.deploy("Sushi", "SUSHI");

const SushiBarContract = await hre.ethers.getContractFactory(
Expand All @@ -33,21 +46,61 @@ describe("SushiYieldSource", function () {
);
sushiBar = await SushiBarContract.deploy(sushi.address);

const SushiYieldSourceContract = await ethers.getContractFactory(
SushiYieldSourceContract = await ethers.getContractFactory(
"SushiYieldSource"
);
yieldSource = await SushiYieldSourceContract.deploy(
sushiBar.address,
sushi.address,
overrides
);

if (!isDeployTest) {
await deploySushiYieldSource(sushiBar.address, sushi.address);
}

amount = toWei("100");

await sushi.mint(wallet.address, amount);
await sushi.mint(wallet2.address, amount.mul(99));
await sushi.connect(wallet2).approve(sushiBar.address, amount.mul(99));
await sushiBar.connect(wallet2).enter(amount.mul(99));
});

describe("constructor()", () => {
before(() => {
isDeployTest = true;
});

after(() => {
isDeployTest = false;
});

it('should succeed to construct yield source', async () => {
await deploySushiYieldSource(sushiBar.address, sushi.address);

expect(await yieldSource.sushiBar()).to.equal(sushiBar.address);
expect(await yieldSource.sushiAddr()).to.equal(sushi.address);
expect(await sushi.allowance(yieldSource.address, sushiBar.address)).to.equal(
ethers.constants.MaxUint256,
);
});

it("should fail if sushiBar address is address 0", async () => {
await expect(
deploySushiYieldSource(ethers.constants.AddressZero, sushi.address)
).to.be.revertedWith("SushiYieldSource/sushiBar-not-zero-address");
});

it("should fail if sushi address is address 0", async () => {
await expect(
deploySushiYieldSource(sushiBar.address, ethers.constants.AddressZero)
).to.be.revertedWith("SushiYieldSource/sushiAddr-not-zero-address");
});
});

describe('approveMaxAmount()', () => {
it('should approve Sushi to spend max uint256 amount', async () => {
expect(await yieldSource.callStatic.approveMaxAmount()).to.eq(true);
expect(await sushi.allowance(yieldSource.address, sushiBar.address)).to.eq(ethers.constants.MaxUint256);
});
});

it("get token address", async function () {
let address = await yieldSource.depositToken();
expect(address == sushi);
Expand All @@ -67,7 +120,7 @@ describe("SushiYieldSource", function () {

it("supplyTokenTo", async function () {
await sushi.connect(wallet).approve(yieldSource.address, amount);
await yieldSource.supplyTokenTo(amount, wallet.address);
expect(await yieldSource.supplyTokenTo(amount, wallet.address)).to.emit(yieldSource, "SuppliedTokenTo");
expect(await sushi.balanceOf(sushiBar.address)).to.eq(amount.mul(100));
expect(await yieldSource.callStatic.balanceOfToken(wallet.address)).to.eq(
amount
Expand All @@ -79,7 +132,7 @@ describe("SushiYieldSource", function () {
await yieldSource.supplyTokenTo(amount, wallet.address);

expect(await sushi.balanceOf(wallet.address)).to.eq(0);
await yieldSource.redeemToken(amount);
expect(await yieldSource.redeemToken(amount)).to.emit(yieldSource, "RedeemedToken");
expect(await sushi.balanceOf(wallet.address)).to.eq(amount);
});

Expand Down

0 comments on commit efce150

Please sign in to comment.