diff --git a/contracts/ethereum/contracts/Token.sol b/contracts/ethereum/contracts/Token.sol index 923f1832f..b7924db69 100644 --- a/contracts/ethereum/contracts/Token.sol +++ b/contracts/ethereum/contracts/Token.sol @@ -165,6 +165,17 @@ contract Token is /// @notice burnFee_ that's paid by the user when they mint uint256 private mintFee_; + /* ~~~~~~~~~~ ADDRESS BLACKLISTING ~~~~~~~~~~ */ + + /// @notice blacklist_ that's used to prevent certain accounts from + /// moving and unwrapping/wrapping funds. + mapping(address => bool) private blacklist_; + + /* ~~~~~~~~~~ EVENTS ~~~~~~~~~~ */ + + /// @dev BlacklistEnabled activated for a specific address + event BlacklistEnabled(address indexed spender, bool status); + /* ~~~~~~~~~~ SETUP FUNCTIONS ~~~~~~~~~~ */ /** @@ -266,6 +277,7 @@ contract Token is uint256 _amount ) internal returns (uint256) { require(noEmergencyMode_, "emergency mode!"); + require(isAddressAllowed(_spender), "address blacklisted"); // take underlying tokens from the user @@ -315,6 +327,9 @@ contract Token is address _beneficiary, uint256 _amount ) internal returns (uint256) { + // check if the account isn't blacklisted + require(isAddressAllowed(_sender), "address blacklisted"); + // take the user's fluid tokens // if the fee amount > 0 and the burn fee is greater than 0, then @@ -387,6 +402,7 @@ contract Token is } /// @dev _transfer is implemented by OpenZeppelin + /// @dev also checks the blacklist and responds accordingly function _transfer( address from, address to, @@ -398,6 +414,8 @@ contract Token is // solhint-disable-next-line reason-string require(to != address(0), "ERC20: transfer to the zero address"); + require(isAddressAllowed(from), "address blacklisted"); + uint256 fromBalance = balances_[from]; // solhint-disable-next-line reason-string @@ -810,11 +828,19 @@ contract Token is return true; } + /** + * @dev transferFrom the address with the sender, if they're allowed. + * @param _from address to send from + * @param _to recipient of the amount + * @param _amount to send + * @dev note that this enforces a blacklist on the sender and _from + */ function transferFrom( address _from, address _to, uint256 _amount ) public returns (bool) { + require(isAddressAllowed(msg.sender), "address blacklisted"); _spendAllowance(_from, msg.sender, _amount); _transfer(_from, _to, _amount); return true; @@ -869,4 +895,27 @@ contract Token is mintFee_ = _mintFee; burnFee_ = _burnFee; } + + /** + * @dev blacklistAddress, only callable by the operator. + * @param _spender to ban using the blacklisting feature + * @param _status of whether or not it's enabled + */ + function blacklistAddress(address _spender, bool _status) public { + require( + msg.sender == operator_ || msg.sender == emergencyCouncil_, + "only operator/emergency council" + ); + require(_spender != address(0), "no zero address"); + emit BlacklistEnabled(_spender, _status); + blacklist_[_spender] = _status; + } + + /** + * @notice isAddressAllowed (are they not blacklisted?) + * @param _account to test + */ + function isAddressAllowed(address _account) public view returns (bool) { + return !blacklist_[_account]; + } } diff --git a/contracts/ethereum/mainnet-constants.ts b/contracts/ethereum/mainnet-constants.ts index 29ff8291f..25aac527a 100644 --- a/contracts/ethereum/mainnet-constants.ts +++ b/contracts/ethereum/mainnet-constants.ts @@ -1,5 +1,5 @@ -import type { Token } from "../types"; +import type { Token } from "./types"; export const USDT_ADDR = "0xdac17f958d2ee523a2206206994597c13d831ec7"; export const CUSDT_ADDR = "0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9"; diff --git a/contracts/ethereum/scripts/deploy-token-beacon-proxy.ts b/contracts/ethereum/scripts/deploy-token-beacon-proxy.ts index c703534a2..e9540e543 100644 --- a/contracts/ethereum/scripts/deploy-token-beacon-proxy.ts +++ b/contracts/ethereum/scripts/deploy-token-beacon-proxy.ts @@ -59,6 +59,9 @@ const main = async () => { let pool: ethers.Contract; + let aToken: string; + let aavePool: string; + switch (backend) { case "compound": const cToken = mustEnv(ENV_COMPOUND_CTOKEN); @@ -72,8 +75,8 @@ const main = async () => { break; case "aaveV2": - const aToken = mustEnv(ENV_AAVE_V2_ATOKEN); - const aavePool = mustEnv(ENV_AAVE_V2_ADDRESS_PROVIDER); + aToken = mustEnv(ENV_AAVE_V2_ATOKEN); + aavePool = mustEnv(ENV_AAVE_V2_ADDRESS_PROVIDER); console.log(`deploying aave v2 pool with beacon ${poolAddress}, atoken ${aToken}, aave pool ${aavePool}`); pool = await hre.upgrades.deployBeaconProxy( poolAddress, @@ -84,8 +87,8 @@ const main = async () => { break; case "aaveV3": - const aToken = mustEnv(ENV_AAVE_V3_ATOKEN); - const aavePool = mustEnv(ENV_AAVE_V3_ADDRESS_PROVIDER); + aToken = mustEnv(ENV_AAVE_V3_ATOKEN); + aavePool = mustEnv(ENV_AAVE_V3_ADDRESS_PROVIDER); console.log(`deploying aave v3 pool with beacon ${poolAddress}, atoken ${aToken}, aave pool ${aavePool}`); pool = await hre.upgrades.deployBeaconProxy( poolAddress, diff --git a/contracts/ethereum/scripts/lootbox-staking-pretend-to-be-address-and-redeem-in-future.ts b/contracts/ethereum/scripts/lootbox-staking-pretend-to-be-address-and-redeem-in-future.ts index cb39c8353..d903be12e 100644 --- a/contracts/ethereum/scripts/lootbox-staking-pretend-to-be-address-and-redeem-in-future.ts +++ b/contracts/ethereum/scripts/lootbox-staking-pretend-to-be-address-and-redeem-in-future.ts @@ -97,7 +97,7 @@ const main = async () => { to: impersonatedAddr }); - const [ fusdcRedeemed, usdcRedeemed, wethRedeemed ] = await redeem(staking); + const [ fusdcRedeemed, usdcRedeemed, wethRedeemed ] = await redeem(staking, 0, 0, 0); console.log( `redeemed for addr ${impersonatedAddr}, fusdc redeemed ${fusdcRedeemed}, usdc redeemed ${usdcRedeemed}, weth redeemed ${wethRedeemed}` diff --git a/contracts/ethereum/test/lootbox-staking-deployed.ts b/contracts/ethereum/test/lootbox-staking-deployed.ts index d1c13cbc6..0d0593ec3 100644 --- a/contracts/ethereum/test/lootbox-staking-deployed.ts +++ b/contracts/ethereum/test/lootbox-staking-deployed.ts @@ -97,7 +97,7 @@ describe("LootboxStaking deployed infra", async () => { method: "hardhat_setBalance", params: [ stakingOperatorAddr, - ethers.constants.MaxUint256.toHexString() + MaxUint256.toHexString() ] }); diff --git a/contracts/ethereum/test/setup-common.ts b/contracts/ethereum/test/setup-common.ts index 6355956d0..73f89e040 100644 --- a/contracts/ethereum/test/setup-common.ts +++ b/contracts/ethereum/test/setup-common.ts @@ -50,6 +50,7 @@ before(async function () { fwEthAccountSigner, veGovSigner, registrySigner, + blacklistedSigner ] = await hre.ethers.getSigners(); const councilAddress = await operatorCouncilSigner.getAddress(); @@ -146,6 +147,7 @@ before(async function () { emergencyCouncil: tokenCouncilSigner, externalOperator: tokenOperatorSigner, externalOracle: externalOracleSigner, + blacklistedSigner: blacklistedSigner, }, operator: { diff --git a/contracts/ethereum/test/setup-mainnet.ts b/contracts/ethereum/test/setup-mainnet.ts index a9e9f95cd..9e36c14ec 100644 --- a/contracts/ethereum/test/setup-mainnet.ts +++ b/contracts/ethereum/test/setup-mainnet.ts @@ -44,6 +44,7 @@ export let bindings: typeof commonBindings & { oracleBoundOperator: ethers.Contract, externalOperator: ethers.Contract, emergencyCouncil: ethers.Contract, + blacklistedSpender: ethers.Contract }, fei: { base: ethers.Contract, @@ -168,6 +169,7 @@ before(async function () { fluidAccount2: contracts.usdt.deployedToken.connect(signers.userAccount2), externalOperator: contracts.usdt.deployedToken.connect(signers.token.externalOperator), emergencyCouncil: contracts.usdt.deployedToken.connect(signers.token.emergencyCouncil), + blacklistedSpender: contracts.usdt.deployedToken.connect(signers.token.blacklistedSigner), oracleBoundOperator: contracts.operator.connect(signers.token.externalOracle), }, fei: { diff --git a/contracts/ethereum/test/token.ts b/contracts/ethereum/test/token.ts index 4b14dc370..e639f9995 100644 --- a/contracts/ethereum/test/token.ts +++ b/contracts/ethereum/test/token.ts @@ -1,4 +1,6 @@ +import * as hre from "hardhat"; + import * as ethers from 'ethers'; import { BigNumber } from 'ethers'; @@ -20,11 +22,15 @@ function fluidityReward(...winners: [string, number][]) { ]]; } +const MaxUint256 = ethers.constants.MaxUint256; + describe("Token", async function () { let fUsdtOperator: ethers.Contract; let fUsdtAccount: ethers.Contract; let fUsdtOracle: ethers.Contract; let fUsdtCouncil: ethers.Contract; + let fUsdtBlacklistedSpender: ethers.Contract; + let fluidToken: string; let accountAddr: string; @@ -38,6 +44,7 @@ describe("Token", async function () { fluidAccount1: fUsdtAccount, oracleBoundOperator: fUsdtOracle, emergencyCouncil: fUsdtCouncil, + blacklistedSpender: fUsdtBlacklistedSpender, }, } = bindings); accountAddr = await signers.userAccount1.getAddress(); @@ -180,4 +187,48 @@ describe("Token", async function () { it("does approvals correctly using eip2612", async () => { }); + + it("ensures that the address is blacklisted and enforced correctly", async () => { + const blacklistedAddr = await signers.token.blacklistedSigner.getAddress(); + const fUsdtOperatorAddr = await fUsdtOperator.signer.getAddress(); + + await hre.network.provider.request({ + method: "hardhat_setBalance", + params: [blacklistedAddr, MaxUint256.toHexString()] + }); + + await fUsdtOperator.blacklistAddress(blacklistedAddr, true); + + await expect(fUsdtBlacklistedSpender.transfer(fUsdtOperatorAddr, MaxUint256)) + .to.be.revertedWith("address blacklisted"); + + fUsdtBlacklistedSpender.approve(fUsdtOperatorAddr, MaxUint256); + + await expect(fUsdtOperator.transferFrom(blacklistedAddr, fUsdtOperatorAddr, MaxUint256)) + .to.be.revertedWith("address blacklisted"); + + await expect(fUsdtBlacklistedSpender.transferFrom(fUsdtOperatorAddr, blacklistedAddr, MaxUint256)) + .to.be.revertedWith("address blacklisted"); + + await expect(fUsdtBlacklistedSpender.erc20In(MaxUint256)) + .to.be.revertedWith("address blacklisted"); + + await expect(fUsdtBlacklistedSpender.erc20Out(MaxUint256)) + .to.be.revertedWith("address blacklisted"); + + await fUsdtOperator.blacklistAddress(blacklistedAddr, false); + + await expect(fUsdtOperator.transferFrom(blacklistedAddr, fUsdtOperatorAddr, MaxUint256)) + .to.be.revertedWith("ERC20: transfer amount exceeds balance"); + + await expect(fUsdtBlacklistedSpender.transferFrom(fUsdtOperatorAddr, blacklistedAddr, MaxUint256)) + .to.be.revertedWith("insufficient allowance"); + + await expect(fUsdtBlacklistedSpender.erc20In(MaxUint256)) + .to.be.revertedWith("SafeERC20: low-level call failed"); + + await expect(fUsdtBlacklistedSpender.erc20Out(MaxUint256)) + .to.be.revertedWith("ERC20: burn amount exceeds balance"); + + }); }); diff --git a/contracts/ethereum/types.ts b/contracts/ethereum/types.ts index 49cf921ea..10f1a9695 100644 --- a/contracts/ethereum/types.ts +++ b/contracts/ethereum/types.ts @@ -63,6 +63,7 @@ export type FluiditySigners = { emergencyCouncil: ethers.Signer, externalOperator: ethers.Signer, externalOracle: ethers.Signer, + blacklistedSigner: ethers.Signer, }, operator: {