Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 78 additions & 21 deletions contracts/token/superfluid/SuperGoodDollar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { ERC777Helper } from "@superfluid-finance/ethereum-contracts/contracts/libs/ERC777Helper.sol";
import { FixedSizeData } from "@superfluid-finance/ethereum-contracts/contracts/libs/FixedSizeData.sol";

import { IGoodDollarCustom } from "./ISuperGoodDollar.sol";
import { SuperToken } from "./SuperToken.sol";
Expand All @@ -29,6 +28,12 @@ contract SuperGoodDollar is
IGoodDollarCustom // without storage
{
error SUPER_GOODDOLLAR_PAUSED();
error SUPER_GOODDOLLAR_BURN_EXCEEDS_ALLOWANCE();
error SUPER_GOODDOLLAR_FALLBACK_FAILED();
error SUPER_GOODDOLLAR_CAP_EXCEEDED();
error SUPER_GOODDOLLAR_NOT_PAUSER();
error SUPER_GOODDOLLAR_NOT_MINTER();
error SUPER_GOODDOLLAR_FEE_EXCEEDS_BALANCE();

// IMPORTANT! Never change the type (storage size) or order of state variables.
// If a variable isn't needed anymore, leave it as padding (renaming is ok).
Expand All @@ -44,7 +49,6 @@ contract SuperGoodDollar is
address public constant getUnderlyingToken = address(0x0);
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

event TransferFee(
address from,
address to,
Expand Down Expand Up @@ -101,8 +105,10 @@ contract SuperGoodDollar is
UUPSProxiable._updateCodeAddress(newAddress);
}

/// override Superfluid agreement function in order to make it pausable
/// that is, no new streams can be started when the contract is paused
/// override Superfluid agreement function in order to make it pausable.
/// NOTE: the CFA never calls this, it writes all flow data through
/// updateAgreementData. This guard therefore covers the IDA (index / subscription
/// creation), not streams - see updateAgreementData below.
function createAgreement(
bytes32 id,
bytes32[] calldata data
Expand All @@ -112,6 +118,62 @@ contract SuperGoodDollar is
super.createAgreement(id, data);
}

/// while paused, block opening or increasing a stream. Closing, decreasing and
/// liquidating are deliberately left open: during an incident we still need to be
/// able to shut malicious streams down.
/// The CFA writes all flow state through updateAgreementData (create, update and
/// delete alike), and writes its flow operator (ACL) data through it as well, so
/// the guard is limited to the CFA and to the flow data layout.
function updateAgreementData(
bytes32 id,
bytes32[] calldata data
) public override(ISuperfluidToken, SuperfluidToken) {
// the agreement class is looked up on the host on each paused call, so that a
// superfluid governance change of the CFA registration is picked up
if (paused() && msg.sender == _cfaV1()) {
_onlyNotIncreasingFlow(id, data);
}
// the write itself stays with SuperfluidToken, so this override can not drift
// from the upstream implementation
super.updateAgreementData(id, data);
}

/// the CFAv1 agreement class as currently registered in the host
function _cfaV1() private view returns (address) {
return
address(
_host.getAgreementClass(
keccak256(
"org.superfluid-finance.agreements.ConstantFlowAgreement.v1"
)
)
);
}

/// CFA flow data packing:
/// | timestamp 32 | flowRate 96 | deposit 64 | owedDeposit 64 |
/// the timestamp is non zero only when flowRate > 0, and is always zero in the
/// flow operator (ACL) data the CFA writes through the same function, so it
/// discriminates between the two single word layouts.
function _onlyNotIncreasingFlow(
bytes32 id,
bytes32[] calldata data
) private view {
uint256 newWord = uint256(data[0]);
// a zero timestamp means a flow being closed, or flow operator data
if (newWord >> 224 == 0) return;
// the previous flow data via the inherited accessor, zeros for a new flow
uint256 oldWord = uint256(getAgreementData(msg.sender, id, 1)[0]);
if (_flowRate(newWord) > _flowRate(oldWord)) {
revert SUPER_GOODDOLLAR_PAUSED();
}
}

/// mirrors ConstantFlowAgreementV1._decodeFlowData, which is internal to the CFA
function _flowRate(uint256 word) private pure returns (int96) {
return int96(int256(word >> 128) & int256(uint256(type(uint96).max)));
}

/// failsafe in case we don't want to trust superfluid host for batch operations
function allowHostOperations()
internal
Expand Down Expand Up @@ -199,10 +261,8 @@ contract SuperGoodDollar is
bool res = super._transferFrom(msg.sender, msg.sender, to, netAmount);
emit ERC677.Transfer(msg.sender, to, netAmount, data);
if (isContract(to)) {
require(
contractFallback(to, netAmount, data),
"Contract fallback failed"
);
if (!contractFallback(to, netAmount, data))
revert SUPER_GOODDOLLAR_FALLBACK_FAILED();
}
return res;
}
Expand Down Expand Up @@ -276,12 +336,8 @@ contract SuperGoodDollar is
) public override(IGoodDollarCustom) onlyMinter returns (bool) {
_onlyNotPaused();

if (cap > 0) {
require(
totalSupply() + amount <= cap,
"Cannot increase supply beyond cap"
);
}
if (cap > 0 && totalSupply() + amount > cap)
revert SUPER_GOODDOLLAR_CAP_EXCEEDED();
_mint(
msg.sender,
to,
Expand All @@ -296,7 +352,8 @@ contract SuperGoodDollar is

function burnFrom(address account, uint256 amount) public {
uint256 currentAllowance = allowance(account, _msgSender());
require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance");
if (currentAllowance < amount)
revert SUPER_GOODDOLLAR_BURN_EXCEEDS_ALLOWANCE();
unchecked {
_approve(account, _msgSender(), currentAllowance - amount);
}
Expand Down Expand Up @@ -368,10 +425,8 @@ contract SuperGoodDollar is
) internal returns (uint256) {
(uint256 txFees, bool senderPays) = getFees(amount, account, recipient);
if (txFees > 0 && !identity.isDAOContract(msg.sender)) {
require(
senderPays == false || amount + txFees <= balanceOf(account),
"Not enough balance to pay TX fee"
);
if (senderPays && amount + txFees > balanceOf(account))
revert SUPER_GOODDOLLAR_FEE_EXCEEDS_BALANCE();
super._transferFrom(account, account, feeRecipient, txFees);
emit TransferFee(account, recipient, amount, txFees, senderPays);
return senderPays ? amount : amount - txFees;
Expand Down Expand Up @@ -408,15 +463,17 @@ contract SuperGoodDollar is
}

function _onlyPauser() internal view {
require(hasRole(PAUSER_ROLE, msg.sender), "not pauser");
if (!hasRole(PAUSER_ROLE, msg.sender))
revert SUPER_GOODDOLLAR_NOT_PAUSER();
}

function _onlyNotPaused() internal view {
if (paused()) revert SUPER_GOODDOLLAR_PAUSED();
}

modifier onlyMinter() {
require(hasRole(MINTER_ROLE, msg.sender), "not minter");
if (!hasRole(MINTER_ROLE, msg.sender))
revert SUPER_GOODDOLLAR_NOT_MINTER();
_;
}
}
7 changes: 5 additions & 2 deletions contracts/token/superfluid/SuperfluidToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { FixedSizeData } from "@superfluid-finance/ethereum-contracts/contracts/
* @dev Modified for SuperGoodDollar
* 1. made createAgreement public and virtual
* 2. added allowHostOperations to disable host actions by G$ governance in case of security issues
* 3. made updateAgreementData public (it is already virtual upstream) so SuperGoodDollar
* can gate streams while paused and still call super
* 4. made getAgreementData public so SuperGoodDollar can read agreement data internally
*/
abstract contract SuperfluidToken is ISuperfluidToken {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just curious here: Does the codebase keep tracking the upstream code? Since I am wary of divergence of code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a fork, and there's no automated tracking today

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It now uses super.updateAgreementData

bytes32 private constant _REWARD_ADDRESS_CONFIG_KEY =
Expand Down Expand Up @@ -246,7 +249,7 @@ abstract contract SuperfluidToken is ISuperfluidToken {
address agreementClass,
bytes32 id,
uint256 dataLength
) external view override returns (bytes32[] memory data) {
) public view override returns (bytes32[] memory data) {
bytes32 slot = keccak256(abi.encode("AgreementData", agreementClass, id));
data = FixedSizeData.loadData(slot, dataLength);
}
Expand All @@ -255,7 +258,7 @@ abstract contract SuperfluidToken is ISuperfluidToken {
function updateAgreementData(
bytes32 id,
bytes32[] calldata data
) external override {
) public virtual override {
address agreementClass = msg.sender;
bytes32 slot = keccak256(abi.encode("AgreementData", agreementClass, id));
FixedSizeData.storeData(slot, data);
Expand Down
7 changes: 3 additions & 4 deletions test/identity/IdentityV4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,6 @@ describe("IdentityV4", () => {
it("should follow reverify schedule and cycle authCount", async () => {
// set timestamp to a fixed point (now) to avoid exclusion of old users
// due to initialDate set in hardhat config
const block = await ethers.provider.getBlock("latest");
await time.setNextBlockTimestamp(Number((Date.now() / 1000).toFixed(0)));
await expect(identity.setReverifyDaysOptions([1, 7, 180])).not.reverted;

Expand Down Expand Up @@ -457,8 +456,8 @@ describe("IdentityV4", () => {
expect(await identity.isWhitelisted(u.address)).to.be.true;
// cleanup (remove whitelisted) to avoid affecting other tests
await identity.removeWhitelisted(u.address);

// restore time to normal flow
time.setNextBlockTimestamp(block.timestamp);
// NOTE: the block time is deliberately not restored here - this test moves the
// chain ~191 days forward and setNextBlockTimestamp cannot rewind. Suites that
// need the original time get it back through their loadFixture snapshot.
});
});
6 changes: 3 additions & 3 deletions test/token/GoodDollar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,9 @@ describe("GoodDollar Token", () => {
it("should not allow to mint beyond cap", async () => {
await expect(unCappedToken.mint(founder.address, 1000)).not.reverted;

await expect(cappedToken.mint(founder.address, 1200)).revertedWith(
/Cannot increase supply beyond cap/
);
await expect(
cappedToken.mint(founder.address, 1200)
).revertedWithCustomError(cappedToken, "SUPER_GOODDOLLAR_CAP_EXCEEDED");
});

it("should collect transaction fee", async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/token/SuperGoodDollar.nohost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe("SuperGoodDollar No Host", async function () {

await expect(
sgd.connect(alice).transfer(bob.address, tenDollars)
).revertedWith(/Not enough balance to pay TX fee/);
).revertedWithCustomError(sgd, "SUPER_GOODDOLLAR_FEE_EXCEEDS_BALANCE");

// mint the extra amount needed for 10% fees
await sgd.mint(alice.address, oneDollar);
Expand Down Expand Up @@ -172,7 +172,7 @@ describe("SuperGoodDollar No Host", async function () {

await expect(
sgd.connect(founder).transferFrom(alice.address, bob.address, tenDollars)
).revertedWith(/Not enough balance to pay TX fee/);
).revertedWithCustomError(sgd, "SUPER_GOODDOLLAR_FEE_EXCEEDS_BALANCE");

// mint the extra amount needed for 10% fees
await sgd.connect(founder).mint(alice.address, oneDollar);
Expand Down
Loading
Loading