tests: reorganize packages (#7)

* tests: reorganize testing packages

* gitignore and minor changes
This commit is contained in:
Federico Kunze
2021-05-11 07:54:55 -04:00
committed by GitHub
parent 117342b1b3
commit 96cad7de9c
115 changed files with 68 additions and 80 deletions
@@ -0,0 +1,31 @@
pragma solidity 0.4.24;
import "./IsContract.sol";
import "./ERCProxy.sol";
contract DelegateProxy is ERCProxy, IsContract {
uint256 internal constant FWD_GAS_LIMIT = 10000;
/**
* @dev Performs a delegatecall and returns whatever the delegatecall returned (entire context execution will return!)
* @param _dst Destination address to perform the delegatecall
* @param _calldata Calldata for the delegatecall
*/
function delegatedFwd(address _dst, bytes _calldata) internal {
require(isContract(_dst));
uint256 fwdGasLimit = FWD_GAS_LIMIT;
assembly {
let result := delegatecall(sub(gas, fwdGasLimit), _dst, add(_calldata, 0x20), mload(_calldata), 0, 0)
let size := returndatasize
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
// revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas.
// if the call returned error data, forward it
switch result case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
@@ -0,0 +1,44 @@
pragma solidity 0.4.24;
import "./DelegateProxy.sol";
import "./DepositableStorage.sol";
contract DepositableDelegateProxy is DepositableStorage, DelegateProxy {
event ProxyDeposit(address sender, uint256 value);
function () external payable {
uint256 forwardGasThreshold = FWD_GAS_LIMIT;
bytes32 isDepositablePosition = DEPOSITABLE_POSITION;
// Optimized assembly implementation to prevent EIP-1884 from breaking deposits, reference code in Solidity:
// https://github.com/aragon/aragonOS/blob/v4.2.1/contracts/common/DepositableDelegateProxy.sol#L10-L20
assembly {
// Continue only if the gas left is lower than the threshold for forwarding to the implementation code,
// otherwise continue outside of the assembly block.
if lt(gas, forwardGasThreshold) {
// Only accept the deposit and emit an event if all of the following are true:
// the proxy accepts deposits (isDepositable), msg.data.length == 0, and msg.value > 0
if and(and(sload(isDepositablePosition), iszero(calldatasize)), gt(callvalue, 0)) {
// Equivalent Solidity code for emitting the event:
// emit ProxyDeposit(msg.sender, msg.value);
let logData := mload(0x40) // free memory pointer
mstore(logData, caller) // add 'msg.sender' to the log data (first event param)
mstore(add(logData, 0x20), callvalue) // add 'msg.value' to the log data (second event param)
// Emit an event with one topic to identify the event: keccak256('ProxyDeposit(address,uint256)') = 0x15ee...dee1
log1(logData, 0x40, 0x15eeaa57c7bd188c1388020bcadc2c436ec60d647d36ef5b9eb3c742217ddee1)
stop() // Stop. Exits execution context
}
// If any of above checks failed, revert the execution (if ETH was sent, it is returned to the sender)
revert(0, 0)
}
}
address target = implementation();
delegatedFwd(target, msg.data);
}
}
@@ -0,0 +1,19 @@
pragma solidity 0.4.24;
import "./UnstructuredStorage.sol";
contract DepositableStorage {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.depositableStorage.depositable")
bytes32 internal constant DEPOSITABLE_POSITION = 0x665fd576fbbe6f247aff98f5c94a561e3f71ec2d3c988d56f12d342396c50cea;
function isDepositable() public view returns (bool) {
return DEPOSITABLE_POSITION.getStorageBool();
}
function setDepositable(bool _depositable) internal {
DEPOSITABLE_POSITION.setStorageBool(_depositable);
}
}
@@ -0,0 +1,10 @@
pragma solidity ^0.4.24;
contract ERCProxy {
uint256 internal constant FORWARDING = 1;
uint256 internal constant UPGRADEABLE = 2;
function proxyType() public pure returns (uint256 proxyTypeId);
function implementation() public view returns (address codeAddr);
}
@@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
contract IsContract {
/*
* NOTE: this should NEVER be used for authentication
* (see pitfalls: https://github.com/fergarrui/ethereum-security/tree/master/contracts/extcodesize).
*
* This is only intended to be used as a sanity check that an address is actually a contract,
* RATHER THAN an address not being a contract.
*/
function isContract(address _target) internal view returns (bool) {
if (_target == address(0)) {
return false;
}
uint256 size;
assembly { size := extcodesize(_target) }
return size > 0;
}
}
@@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
@@ -0,0 +1,24 @@
pragma solidity 0.4.24;
import "../DepositableDelegateProxy.sol";
contract DepositableDelegateProxyMock is DepositableDelegateProxy {
address private implementationMock;
function enableDepositsOnMock() external {
setDepositable(true);
}
function setImplementationOnMock(address _implementationMock) external {
implementationMock = _implementationMock;
}
function implementation() public view returns (address) {
return implementationMock;
}
function proxyType() public pure returns (uint256 proxyTypeId) {
return UPGRADEABLE;
}
}
@@ -0,0 +1,8 @@
pragma solidity 0.4.24;
contract EthSender {
function sendEth(address to) external payable {
to.transfer(msg.value);
}
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
@@ -0,0 +1,17 @@
pragma solidity 0.4.24;
contract ProxyTargetWithoutFallback {
event Pong();
function ping() external {
emit Pong();
}
}
contract ProxyTargetWithFallback is ProxyTargetWithoutFallback {
event ReceivedEth();
function () external payable {
emit ReceivedEth();
}
}
@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
+16
View File
@@ -0,0 +1,16 @@
{
"name": "proxy",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"chai": "^4.2.0",
"truffle": "^5.1.42",
"web3": "^1.2.11"
}
}
@@ -0,0 +1,155 @@
const { bn } = require('@aragon/contract-helpers-test')
const { assertAmountOfEvents, assertEvent, assertRevert, assertOutOfGas, assertBn } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const DepositableDelegateProxyMock = artifacts.require('DepositableDelegateProxyMock')
const EthSender = artifacts.require('EthSender')
const ProxyTargetWithoutFallback = artifacts.require('ProxyTargetWithoutFallback')
const ProxyTargetWithFallback = artifacts.require('ProxyTargetWithFallback')
const TX_BASE_GAS = 21000
const SEND_ETH_GAS = TX_BASE_GAS + 9999 // <10k gas is the threshold for depositing
const PROXY_FORWARD_GAS = TX_BASE_GAS + 2e6 // high gas amount to ensure that the proxy forwards the call
const FALLBACK_SETUP_GAS = 100 // rough estimation of how much gas it spends before executing the fallback code
const SOLIDITY_TRANSFER_GAS = 2300
contract('DepositableDelegateProxy', ([ sender ]) => {
let ethSender, proxy, target, proxyTargetWithoutFallbackBase, proxyTargetWithFallbackBase
// Initial setup
before(async () => {
ethSender = await EthSender.new()
proxyTargetWithoutFallbackBase = await ProxyTargetWithoutFallback.new()
proxyTargetWithFallbackBase = await ProxyTargetWithFallback.new()
})
beforeEach(async () => {
proxy = await DepositableDelegateProxyMock.new()
target = await ProxyTargetWithFallback.at(proxy.address)
})
const itForwardsToImplementationIfGasIsOverThreshold = () => {
context('when implementation address is set', () => {
const itSuccessfullyForwardsCall = () => {
it('forwards call with data', async () => {
const receipt = await target.ping({ gas: PROXY_FORWARD_GAS })
assertAmountOfEvents(receipt, 'Pong')
})
}
context('when implementation has a fallback', () => {
beforeEach(async () => {
await proxy.setImplementationOnMock(proxyTargetWithFallbackBase.address)
})
itSuccessfullyForwardsCall()
it('can receive ETH [@skip-on-coverage]', async () => {
const receipt = await target.sendTransaction({ value: 1, gas: SEND_ETH_GAS + FALLBACK_SETUP_GAS })
assertAmountOfEvents(receipt, 'ReceivedEth')
})
})
context('when implementation doesn\'t have a fallback', () => {
beforeEach(async () => {
await proxy.setImplementationOnMock(proxyTargetWithoutFallbackBase.address)
})
itSuccessfullyForwardsCall()
it('reverts when sending ETH', async () => {
await assertRevert(target.sendTransaction({ value: 1, gas: PROXY_FORWARD_GAS }))
})
})
})
context('when implementation address is not set', () => {
it('reverts when a function is called', async () => {
await assertRevert(target.ping({ gas: PROXY_FORWARD_GAS }))
})
it('reverts when sending ETH', async () => {
await assertRevert(target.sendTransaction({ value: 1, gas: PROXY_FORWARD_GAS }))
})
})
}
const itRevertsOnInvalidDeposits = () => {
it('reverts when call has data', async () => {
await proxy.setImplementationOnMock(proxyTargetWithoutFallbackBase.address)
await assertRevert(target.ping({ gas: SEND_ETH_GAS }))
})
it('reverts when call sends 0 value', async () => {
await assertRevert(proxy.sendTransaction({ value: 0, gas: SEND_ETH_GAS }))
})
}
context('when proxy is set as depositable', () => {
beforeEach(async () => {
await proxy.enableDepositsOnMock()
})
context('when call gas is below the forwarding threshold', () => {
const value = bn(100)
const assertSendEthToProxy = async ({ value, gas, shouldOOG }) => {
const initialBalance = bn(await web3.eth.getBalance(proxy.address))
const sendEthAction = () => proxy.sendTransaction({ from: sender, gas, value })
if (shouldOOG) {
await assertOutOfGas(sendEthAction())
assertBn(bn(await web3.eth.getBalance(proxy.address)), initialBalance, 'Target balance should be the same as before')
} else {
const receipt = await sendEthAction()
assertBn(bn(await web3.eth.getBalance(proxy.address)), initialBalance.add(value), 'Target balance should be correct')
assertAmountOfEvents(receipt, 'ProxyDeposit', { decodeForAbi: DepositableDelegateProxyMock.abi })
assertEvent(receipt, 'ProxyDeposit', { decodeForAbi: DepositableDelegateProxyMock.abi, expectedArgs: { sender, value } })
return receipt
}
}
it('can receive ETH', async () => {
await assertSendEthToProxy({ value, gas: SEND_ETH_GAS })
})
it('cannot receive ETH if sent with a small amount of gas [@skip-on-coverage]', async () => {
const oogDecrease = 250
// deposit cannot be done with this amount of gas
const gas = TX_BASE_GAS + SOLIDITY_TRANSFER_GAS - oogDecrease
await assertSendEthToProxy({ shouldOOG: true, value, gas })
})
it('can receive ETH from contract [@skip-on-coverage]', async () => {
const receipt = await ethSender.sendEth(proxy.address, { value })
assertAmountOfEvents(receipt, 'ProxyDeposit', { decodeForAbi: proxy.abi })
assertEvent(receipt, 'ProxyDeposit', { decodeForAbi: proxy.abi, expectedArgs: { sender: ethSender.address, value } })
})
itRevertsOnInvalidDeposits()
})
context('when call gas is over forwarding threshold', () => {
itForwardsToImplementationIfGasIsOverThreshold()
})
})
context('when proxy is not set as depositable', () => {
context('when call gas is below the forwarding threshold', () => {
it('reverts when depositing ETH', async () => {
await assertRevert(proxy.sendTransaction({ value: 1, gas: SEND_ETH_GAS }))
})
itRevertsOnInvalidDeposits()
})
context('when call gas is over forwarding threshold', () => {
itForwardsToImplementationIfGasIsOverThreshold()
})
})
})
@@ -0,0 +1,23 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.4.24",
settings: {
optimizer: {
enabled: true,
runs: 10000,
},
},
},
},
}